精华内容
参与话题
问答
  • JAVA高并发的三种实现

    万次阅读 多人点赞 2018-07-23 10:50:59
    是用它可以解决一切并发问题,但是,对于系统吞吐量要求更的话,我们这提供几个小技巧。帮助大家减小锁颗粒度,提高并发能力。 初级技巧-乐观锁 乐观锁使用的场景是,读不会冲突,写会冲突。同时读的频率远大于...

    提到锁,大家肯定想到的是sychronized关键字。是用它可以解决一切并发问题,但是,对于系统吞吐量要求更高的话,我们这提供几个小技巧。帮助大家减小锁颗粒度,提高并发能力。

    初级技巧-乐观锁

    乐观锁使用的场景是,读不会冲突,写会冲突。同时读的频率远大于写。

     悲观锁的实现

    悲观的认为所有代码执行都会有并发问题,所以将所有代码块都用sychronized锁住

    乐观锁的实现

    乐观的认为在读的时候不会产生冲突为题,在写时添加锁。所以解决的应用场景是读远大于写时的场景。

    中级技巧-String.intern()

    乐观锁不能很好的解决大量的写冲突的问题,但是很多场景下,锁只是针对某个用户或者某个订单。 比如一个用户先创建session,才能进行后面的操作,但是由于网络的问题,创建session的请求和后续请求几乎同时到达,而并行线程可能会先处理后面的请求。一般情况需要对用户sessionMap加锁,比如上面的乐观锁。在这样的场景下,可以将锁限定在用户本身上,即原来的

    这个比较类似行锁和数据库表锁的概念。显然行锁的并发能力比表锁的高很多。

    实用String.intern();是这种方式的具体实现。类String维护了一个字符串池。当调用intern方法时,如果池已经包含一个等于此String对象的字符串(该对象由equals(Object)方法确定),则返回池中的字符串。可见,当String 相同时,总返回同一个对象,因此就实现了对同一用户加锁。由于所的颗粒度局限于具体用户,使得系统获得最大程度的并发。

    CopyOnWriteMap?

     

    既然说到了“类似于数据库中的行锁的概念”,就不得不提一下MVCC,Java中CopyOnWrite类实现了MVCC。Copy On Write是这样一种机制。当我们读取共享数据的时候,直接读取,不需要同步。当我们修改数据的时候,我们就把当前数据Copy一份副本,然后在这个副本 上进行修改,完成之后,再用修改后的副本,替换掉原来的数据。这种方法就叫做Copy On Write。

     

    但是,,,JDK并没有提供CopyOnWriteMap,为什么?下面有个很好的回答,那就是已经有了ConcurrentHashMap,为什么还需要CopyOnWriteMap?

     

    高级技巧 - 类ConcurrentHashMap

    String.inter()的缺陷是类 String 维护一个字符串池是放在JVM perm区的,如果用户数特别多,导致放入字符串池的String不可控,有可能导致OOM错误或者过多的Full GC。怎么样能控制锁的个数,同时减小粒度锁呢?直接使用Java ConcurrentHashMap?或者你想加入自己更精细的控制?那么可以借鉴ConcurrentHashMap的方式,将需要加锁的对象分为多个bucket,每个bucket加一个锁,伪代码如下:

     

     

     

     

     

     

    展开全文
  • java高并发

    千次阅读 多人点赞 2018-02-02 18:11:32
    并发

    转载地址:https://www.cnblogs.com/lr393993507/p/5909804.html

        对于开发的网站,如果网站的访问量非常大,那么我们应该考虑相关的、并发访问问题,并发是绝大部分程序员头疼的问题;

    为了更好的理解并发和同步,先明白两个重要的概念:异步和同步;

     1、同步和异步的区别和联系

    所谓同步就是执行完一个函数后方法后,一直等待系统返回值或消息,这是程序是阻塞的,只有接受到返回值或者消息才会往下执行其他命令

    异步,执行完函数或方法后,不必阻塞的等待系统返回值或消息,只需向系统委托一个异步过程,那么当系统接受到返回值或者消息,系统会

    自动触发异步过程,完成一个完整的流程

    同步在一定程度上可以看成单线程,线程请求一个方法后就等待这个方法给回复,否则不会继续执行下去(死心眼);

    异步在一定程度上可以看成多线程,请求一个方法后就不管了,继续执行其他的方法

    同步就是事情一件一件的做,异步就是做一件事,不影响做其他事情。

    例:吃饭和说话是同步的,只能一件一件的做,因为只有一张嘴

    吃饭和听音乐是异步的,吃饭和听音乐互不影响

          

         对于java程序员,我们经常会听到同步关键字synchronized,假如这个同步的监视对象是类,那么如果一个对象访问类里面的同步方法是,另一个对象

    也访问这个类里面的同步方法,这是就会进入阻塞状态,只有等前一个对象执行完同步方法后,后一个对象才能继续执行该方法,这就是同步。相反,

    如果没有被同步关键字synchronize修饰,两个对象可以在同一时间访问同一方法,这就是异步。

        脏数据

    脏读就是指当一个事物正在访问数据,并且对数据做了修改,但是数据修改并未提交到数据库,而另一个事物也在访问这个数据,然后使用数据,因为

    这个数据是还没提交的数据,所以另一个事务读取的数据就是脏数据(dirty data),依据脏数据做的操作是不正确的

    不可重复读

    不可重复读是指一个事务内多次读取同一个数据,在这个事务还没有结束的时候,另一个事务也访问同一数据并做了修改,这时第一个事务中两次读取的

    数据可能是不一样的,这样就发生在 一个事务内读取的数据不一样,因此成为不可重复读


    二、如何处理并发和同步

    处理并发和同步问题主要是通过锁机制,锁机制主要有两个层面

    一种是代码层次的,如java的同步锁,典型的就是同步关键字synchronize,这里不做过多的讲解:

    感兴趣的可以参考:http://www.cnblogs.com/xiohao/p/4151408.html

    另一种是数据库层次的,比较典型的是悲观锁和乐观锁,这里重点讲解的就是悲观锁(传统的物理锁)和乐观锁

    悲观锁(Prssimistic Locking)

    悲观锁:正如其名,它是指对数据被外界(包括本系统当前的其他事务和外部系统的事务处理)修改保持保守态度,因此在数据处理过程中,

    将数据处于锁定状态

    悲观锁的实现机制,往往依靠数据库提供的锁机制(也只有数据库层的锁机制才能真正保持数据访问的排他性,否则即使在本系统中实现了加锁机制,也无法

    保证外部系统不会修改数据)

    一个典型的依赖数据库的悲观锁调用:

    select * form account where  name=‘admin’  for  update

    这条sql语句锁定account表中所有符合条件查询的记录,在事物提交前(事务提交时会释放是事务过程中的锁),外界无法修改这些记录

    hibernate的悲观锁也是基于数据库的锁机制实现的

    下列代码实现了hibernate的悲观锁

    String hql="from User where  user.name=''admin";

    Query query=session.createQuery(hql);

    query.setLock('user",LockMode.UPGRADE);//加锁

    List queryList=query.list()

    query.setLock对查询语句中,特定别名所对应的记录进行加锁,对返回的user记录进行加锁

    观察运行期 Hibernate 生成的 SQL 语句: 
          select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id
          as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex
          from t_user tuser0_ where (tuser0_.name='Erica' ) for update
         这里 Hibernate 通过使用数据库的 for update 子句实现了悲观锁机制。 
          Hibernate 的加锁模式有: 
          Ø LockMode.NONE : 无锁机制。 
          Ø LockMode.WRITE : Hibernate 在 Insert 和 Update 记录的时候会自动获取
          Ø LockMode.READ : Hibernate 在读取记录的时候会自动获取。 
          以上这三种锁机制一般由 Hibernate 内部使用,如 Hibernate 为了保证 Update
          过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。 
          Ø LockMode.UPGRADE :利用数据库的 for update 子句加锁。 
          Ø LockMode. UPGRADE_NOWAIT : Oracle 的特定实现,利用 Oracle 的 for
          update nowait 子句实现加锁。 
          上面这两种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现: 
          Criteria.setLockMode
          Query.setLockMode
          Session.lock

    注意:只有在查询开始之前(hibernate生成sql之前)加锁 ,才会真正通过数据库的锁机制进行加锁,否则数据已经通过不包含for uodate 

    的sql加载进来,此时无法进行加锁

      为了更好的理解select... for update的锁表的过程,本人将要以mysql为例,进行相应的讲解

          1、要测试锁定的状况,可以利用MySQL的Command Mode ,开二个视窗来做测试。

              表的基本结构如下:

              

     

               表中内容如下:

               

     

              开启两个测试窗口,在其中一个窗口执行select * from ta for update0

              然后在另外一个窗口执行update操作如下图:

              

              等到一个窗口commit后的图片如下:

              

               到这里,悲观锁机制你应该了解一些了吧~

    注意:for  update 要放在mysql的事物中,即begin和end之间,否则不起作用

       至于是锁住整个表还是锁住选中的行,请参考:

               http://www.cnblogs.com/xiohao/p/4385768.html

                至于hibernate中的悲观锁使用起来比较简单,这里就不写demo了~感兴趣的自己查一下就ok了~


    乐观锁

    相对于悲观锁,乐观锁采取更加宽松的加锁机制。悲观锁主要是通过数据库的加锁机制实现,保证操作最大程度的独占性,

    但随之而来的是数据库性能的大量开销,对于长事务而言,这样的开销是无法承受的。如一个金融系统,当某个操作员读取用户

    的数据,并在读出的用户数据的基础上进 行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 中

    (从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于

    加锁状态,可以想见,如果面对几 百上千个并发,这样的情况将导致怎样的后果。 乐观锁机制在一定程度上解决了这个问题。

    乐观锁大多是同过数据库版本(version)记录机制实现的,即为数据增加一个版本标识,在基于数据库表的解决方案中,一般是

    在表中增加一个version字段来实现,读取数据时将版本号一起读取,在更新操作时将版本号加1,将提交的数据的版本号和数据库中

    的数据版本号进行比较,若大于数据库中数据的版本号,则提交更新数据,否则认为是过期数据,无法提交,抛出异常。


    对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )

    为 $100 。操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 2 在操作员 A 操作的过程中,操作员B

    也读入此用户信息( version=1 ),并 从其帐户余额中扣除 $20 ( $100-$20 )。 3 操作员 A 完成了修改工作,将数据版本号加一(

    version=2 ),连同帐户扣 除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被

    更新,数据库记录 version 更新为 2 。 4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数 据( balance=$80 ),

    但此时比对数据库记录版本时发现,操作员 B 提交的 数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记 录

    当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于version=1 的旧数据修改的

    结果覆盖操作 员 A 的操作结果的可能。 从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操

    作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。

    需要注意的是乐观锁是基于数据存储的逻辑,因此具备一定的局限性,如在上例中,由于乐观锁是在我们的系统内实现的,来自外部系统的

    用户余额更新操作不受系统的限制,因此可能会造成脏数据更新到数据库中。在系统设计时,我们应该充分考虑这些情况,并作出相应的调整,如将

    乐观锁放在数据库的存储过程中,对外开放基于此存储过程的更新途径,而不是直接将数据库表对外公开。 hibernate中数据访问引擎中实现

    了乐观锁,如果不考虑外部系统的数据库的操作,可以考察通过hibernate提供的透明乐观锁实现,将大大提升生产力

    User.hbm.xml


    复制代码
    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC
            "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
            "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
     
    <hibernate-mapping package="com.xiaohao.test">
     
        <class name="User"  table="user" optimistic-lock="version" >
                  <id name="id">
                <generator class="native" />
            </id>
            <!--version标签必须跟在id标签后面-->
            <version column="version" name="version"  />
            <property name="userName"/>
            <property name="password"/>
                     
        </class>
         
     
    </hibernate-mapping>

    注意:version节点必须出现在id节点后

    这里我们声明了一个 version 属性,用于存放用户的版本信息,保存在 User 表的version中 
    optimistic-lock 属性有如下可选取值: 
    Ø none
    无乐观锁 
    Ø version
    通过版本机制实现乐观锁 
    Ø dirty
    通过检查发生变动过的属性实现乐观锁 
    Ø all
    通过检查所有属性实现乐观锁 
    其中通过 version 实现的乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也 
    是 Hibernate 中,目前唯一在数据对象脱离 Session 发生修改的情况下依然有效的锁机 
    制。因此,一般情况下,我们都选择 version 方式作为 Hibernate 乐观锁实现机制。

    2 . 配置文件hibernate.cfg.xml和UserTest测试类


       hibernate.cfg.xml


    复制代码
    <!DOCTYPE hibernate-configuration PUBLIC
            "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
            "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
     
    <hibernate-configuration>
    <session-factory>
     
        <!-- 指定数据库方言 如果使用jbpm的话,数据库方言只能是InnoDB-->
        <property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
        <!-- 根据需要自动创建数据表 -->
        <property name="hbm2ddl.auto">update</property>
        <!-- 显示Hibernate持久化操作所生成的SQL -->
        <property name="show_sql">true</property>
        <!-- 将SQL脚本进行格式化后再输出 -->
        <property name="format_sql">false</property>
        <property name="current_session_context_class">thread</property>
     
     
        <!-- 导入映射配置 -->
        <property name="connection.url">jdbc:mysql:///user</property>
        <property name="connection.username">root</property>
        <property name="connection.password">123456</property>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <mapping resource="com/xiaohao/test/User.hbm.xml" />
     
     
     
    </session-factory>
    </hibernate-configuration>
    复制代码
    UserTest.java


    复制代码
    package com.xiaohao.test;
     
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    import org.hibernate.cfg.Configuration;
     
    public class UserTest {
        public static void main(String[] args) {
            Configuration conf=new Configuration().configure();
            SessionFactory sf=conf.buildSessionFactory();
            Session session=sf.getCurrentSession();
            Transaction tx=session.beginTransaction();
    //      User user=new User("小浩","英雄");
    //      session.save(user);
    //       session.createSQLQuery("insert into user(userName,password) value('张英雄16','123')")
    //                  .executeUpdate();
            User user=(User) session.get(User.class, 1);
            user.setUserName("221");
    //      session.save(user);
         
            System.out.println("恭喜您,用户的数据插入成功了哦~~");
            tx.commit();
        }
     
    }
    复制代码
    每次对 TUser 进行更新的时候,我们可以发现,数据库中的 version 都在递增。


     


    下面我们将要通过乐观锁来实现一下并发和同步的测试用例:


    这里需要使用两个测试类,分别运行在不同的虚拟机上面,以此来模拟多个用户同时操作一张表,同时其中一个测试类需要模拟长事务


    UserTest.java


    复制代码
    package com.xiaohao.test;
     
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    import org.hibernate.cfg.Configuration;
     
    public class UserTest {
        public static void main(String[] args) {
            Configuration conf=new Configuration().configure();
            SessionFactory sf=conf.buildSessionFactory();
            Session session=sf.openSession();
    //      Session session2=sf.openSession();
            User user=(User) session.createQuery(" from User user where user=5").uniqueResult();
    //      User user2=(User) session.createQuery(" from User user where user=5").uniqueResult();
            System.out.println(user.getVersion());
    //      System.out.println(user2.getVersion());
            Transaction tx=session.beginTransaction();
            user.setUserName("101");
            tx.commit();
             
            System.out.println(user.getVersion());
    //      System.out.println(user2.getVersion());
    //      System.out.println(user.getVersion()==user2.getVersion());
    //      Transaction tx2=session2.beginTransaction();
    //      user2.setUserName("4468");
    //      tx2.commit();
         
        }
     
    }
    复制代码
     


    UserTest2.java


    复制代码
    package com.xiaohao.test;
     
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    import org.hibernate.cfg.Configuration;
     
    public class UserTest2 {
        public static void main(String[] args) throws InterruptedException {
            Configuration conf=new Configuration().configure();
            SessionFactory sf=conf.buildSessionFactory();
            Session session=sf.openSession();
    //      Session session2=sf.openSession();
            User user=(User) session.createQuery(" from User user where user=5").uniqueResult();
            Thread.sleep(10000);
    //      User user2=(User) session.createQuery(" from User user where user=5").uniqueResult();
            System.out.println(user.getVersion());
    //      System.out.println(user2.getVersion());
            Transaction tx=session.beginTransaction();
            user.setUserName("100");
            tx.commit();
             
            System.out.println(user.getVersion());
    //      System.out.println(user2.getVersion());
    //      System.out.println(user.getVersion()==user2.getVersion());
    //      Transaction tx2=session2.beginTransaction();
    //      user2.setUserName("4468");
    //      tx2.commit();
         
        }
     
    }
    复制代码
     


    操作流程及简单讲解: 首先启动UserTest2.java测试类,在执行到Thread.sleep(10000);这条语句的时候,当前线程会进入睡眠状态。在10秒钟之内
      启动UserTest这个类,在到达10秒的时候,我们将会在UserTest.java中抛出下面的异常:

    复制代码
    Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.xiaohao.test.User#5]
        at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1932)
        at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2576)
        at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2476)
        at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2803)
        at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:113)
        at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
        at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265)
        at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185)
        at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
        at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
        at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
        at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383)
        at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133)
        at com.xiaohao.test.UserTest2.main(UserTest2.java:21)

     UserTest2代码将在 tx.commit() 处抛出 StaleObjectStateException 异 常,并指出版本检查失败,当前事务正在试图提交

    一个过期数据。通过捕捉这个异常,我 们就可以在乐观锁校验失败时进行相应处理

     


     3、常见并发同步案例分析

        案例一:订票系统案例,某航班只有一张机票,假定有1w个人打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑
                   的并发读写问题)
        问题,1w个人来访问,票没出去前要保证大家都能看到有票,不可能一个人在看到票的时候别人就不能看了。到底谁能抢到,那得看这个人的“运气”(网
                 络快慢等)
    其次考虑的问题,并发,1w个人同时点击购买,到底谁能成交?总共只有一张票。
    首先我们容易想到和并发相关的几个方案 :
    锁同步同步更多指的是应用程序的层面,多个线程进来,只能一个一个的访问,java中指的是syncrinized关键字。锁也有2个层面,一个是java中谈到的对
    象锁,用于线程同步;另外一个层面是数据库的锁;如果是分布式的系统,显然只能利用数据库端的锁来实现。
    假定我们采用了同步机制或者数据库物理锁机制,如何保证1w个人还能同时看到有票,显然会牺牲性能,在高并发网站中是不可取的。使用hibernate后我们
    提出了另外一个概念:乐观锁、悲观锁(即传统的物理锁);
    采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的并发可读性又保证保存数据的排他性,保
    证性能的同时解决了并发带来的脏数据问题。
    hibernate中如何实现乐观锁:
    前提:在现有表当中增加一个冗余字段,version版本号, long类型
    原理:
    1)只有当前版本号》=数据库表版本号,才能提交
    2)提交成功后,版本号version ++
    实现很简单:在ormapping增加一属性optimistic-lock="version"即可,以下是样例片段
    <hibernate-mapping>
    <class name="com.insigma.stock.ABC" optimistic-lock="version" table="T_Stock" schema="STOCK">

    案例二、股票交易系统、银行系统,大数据量你是如何考虑的

    首先,股票交易系统的行情表,每几秒钟就有一个行情记录产生,一天下来就有(假定行情3秒一个) 股票数量×20×60*6 条记录,一月下来这个表记录数

    量多大? oracle中一张表的记录数超过100w后 查询性能就很差了,如何保证系统性能?

    再比如,中国移动有上亿的用户量,表如何设计?把所有用于存在于一个表么?

    所以,大数量的系统,必须考虑拆分表(表名字不一样,表结构一样),通用的拆分方式有以下几种,视情况区别

    1、按业务分,比如 手机号的表,我们可以考虑 130开头的作为一个表,131开头的另外一张表 以此类推

    2、利用oracle表拆分机制做分表

    3、如果是交易系统,我们可以考虑按时间轴拆分,当日数据一个表,历史数据一个表,历史数据的报表和查询不会影响当日的交易

    当然,表拆分之后我们的应用也应做相应的调整,单纯的or-mapping可能要改动,比如部分业务通过存储过程等。

    此外,我们还得考虑缓存

    这里的缓存不是hibernate的一级二级缓存,而是独立于应用的,依然是内存的读取,如果可以减少与数据库的交互,可以大大的减小数据库的压力,

    比如一个电子商务系统的商品搜索,可以考虑将经常搜索的商品列表放到缓存中,这样可以极大的增加性能。

    简单的缓存大家可以理解为自己做一个hashmap,把常访问的数据做一个key,value是第一次从数据库搜索出来的值,下次访问

    就可以从map里读取,而不读数据库;专业些的目前有独立的缓存框架比如memcached 等,可独立部署成一个缓存服务器。


    4、常见的提高高并发下访问效率的手段

    首先先了解高并发的瓶颈所在

    1、网络带宽不够

    2、web线程连接数不足

    3、数据库连接查询太慢

    根据不同的情况可用以下的方式应对

    1、增加网络带宽,dns域名解析分发多台服务器

    2、负载均衡,前置代理服务器如Nginx,Apache等等

    3、数据库查询优化,分表,读写分离等等

     最后复制一些在高并发下面需要常常需要处理的内容:

    尽量使用缓存,包括用户缓存、信息缓存,多花点内存做缓存,减少与数据库的交互,提交性能

    用jprofiler等工具找出性能瓶颈,减少额外的开销。

    优化数据库查询语句,减少直接使用hibernate等工具的直接生成语句(仅耗时较长的查询做优化)

    优化数据库结构,多做索引,提高查询效率

    统计的功能尽量做缓存,或按每天一统计或定时统计相关报表,避免需要时进行统计的功能。

    能使用静态页面的地方尽量使用,减少容器的解析(尽量将动态内容生成静态html来显示)。

    解决以上问题后,使用服务器集群来解决单台的瓶颈问题。


    ----------------------java高并发 如何解决  用什么方式解决--------------------

    高并发的解决方式主要有两种:

    1、使用缓存

    2、使用静态页面

    还有就是从最基础的地方优化我们减少写代码不必要的资源浪费

    1、不要频繁的new对象,对于在应用中只需要存在一个实例的类采用单例模式,对于String的连接使用

    StringBulider或者StringBuffer,对于工具类采用静态方法访问

    2、避免使用错误的方式,如Exception可以控制方法推出,但是Exception要保留stacktrace消耗性能,除非必

         要不要使用 instanceof做条件判断,尽量使用比的条件判断方式.使用JAVA中效率高的类,比如ArrayList比Vector性能好。)


    首先缓存技术我一直没有使用过,我觉得应该是在用户请求时将数据保存在缓存中,下次请求时会检测缓存中是否有数据存在,防止多次请求服务器,导致服务器性能降低,严重导致服务器崩溃,这只是我自己的理解,详细的资料还是需要在网上收集;

     

    使用生成静态页面我想大家应该不模式,我们见过很多网站当在请求的时候页面的后最已经变了,如“http://developer.51cto.com/art/201207/348766.htm”该页面其实是一个服务器请求地址,在转换成htm后,访问速度将提升,因为静态页面不带有服务器组件;在这里我就多多介绍一下:

    一、什么是页面静态化:

    简 单的说,我们如果访问一个链接 ,服务器对应的模块会处理这个请求,转到对应的jsp界面,最后生成我们想要看到的数据。这其中的缺点是显而易见的:因为每次请求服务器都会进行处理,如 果有太多的高并发请求,那么就会加重应用服务器的压力,弄不好就把服务器 搞down 掉了。那么如何去避免呢?如果我们把对 test.do 请求后的结果保存成一个 html 文件,然后每次用户都去访问 ,这样应用服务器的压力不就减少了?

    那么静态页面从哪里来呢?总不能让我们每个页面都手动处理吧?这里就牵涉到我们要讲解的内容了,静态页面生成方案… 我们需要的是自动的生成静态页面,当用户访问 ,会自动生成 test.html ,然后显示给用户。

    二、下面我们在简单介绍一下要想掌握页面静态化方案应该掌握的知识点:

    1、 基础- URL Rewrite

    什么是 URL Rewrite 呢 ? URL 重写。用一个简单的例子来说明问题:输入网址 ,但是实际上访问的却是 abc.com/test.action,那我们就可以说 URL 被重写了。这项技术应用广泛,有许多开源的工具可以实现这个功能。

    2、 基础- Servlet web.xml

    如果你还不知道 web.xml 中一个请求和一个 servlet 是如何匹配到一起的,那么请搜索一下 servlet 的文档。这可不是乱说呀,有很多人就认为 /xyz/*.do 这样的匹配方式能有效。

    如果你还不知道怎么编写一个 servlet ,那么请搜索一下如何编写 servlet.这可不是说笑呀,在各种集成工具漫天飞舞的今天,很多人都不会去从零编写一个 servlet了

    三、基本的方案介绍

    java高并发,如何解决,什么方式解决 - 我学坊 - 励志-我学坊
    其中,对于 URL Rewriter的部分,可以使用收费或者开源的工具来实现,如果 url不是特别的复杂,可以考虑在 servlet 中实现,那么就是下面这个样子:

     

    java高并发,如何解决,什么方式解决 - 我学坊 - 励志-我学坊
     
    总 结:其实我们在开发中都很少考虑这种问题,直接都是先将功能实现,当一个程序员在干到1到2年,就会感觉光实现功能不是最主要的,安全性能、质量等等才是 一个开发人员最该关心的。今天我所说的是高并发。
    我的解决思路是:
    1、采用分布式应用设计
    2、分布式缓存数据库
    3、代码优化


    Java高并发的例子:

    具体情况是这样: 通过java和数据库,自己实现序列自动增长。
    实现代码大致如下:
     id_table表结构, 主要字段:

     id_name  varchar2(16);
     id_val  number(16,0);
     id_prefix  varchar2(4);
       


    复制代码
    //操作DB 
       public synchronized String nextStringValue(String id){
            SqlSession sqlSess = SqlSessionUtil.getSqlSession();
            sqlSess.update("update id_table set id_val = id_val + 1 where id_name="+id);
            Map map = sqlSess.getOne("select id_name, id_prefix, id_val from id_table where id_name="+ id);
            BigDecimal val = (BigDecimal) map.get("id_val");
          //id_val是具体数字,rePack主要是统一返回固定长度的字符串;如:Y0000001, F0000001, T0000001等
            String idValue = rePack(val, map); 
            return idValue;
      }
       
      //公共方法
    public class IdHelpTool{
         public static String getNextStringValue(String idName){
              return getXX().nextStringValue(idName);
        }
    }
    复制代码

    具体使用者,都是通过类似这种方式:IdHelpTool.getNextStringValue("PAY_LOG");来调用。

    问题:
          (1) 当出现并发时, 有时会获取重复的ID;
          (2) 由于服务器做了相关一些设置,有时调用这个方法,好像还会导致超时。

             为了解决问题(1), 考虑过在方法getNextStringValue上,也加上synchronized , 同步关键字过多,会不会更导致超时?
    跪求大侠提供个解决问题的大概思路!!!

    解决思路一:

    1、推荐 https://github.com/adyliu/idcenter
    2、可以通过第三方redis来实现。

     

    解决思路一:

    1、出现重复ID,是因为脏读了,并发的时候不加 synchronized  比如会出现问题

    2、但是加了 synchronized  ,性能急剧下降了,本身 java 就是多线程的,你把它单线程使用,不是明智的选择,同时,如果分布式部署的时候,加了 synchronized  也无法控制并发

    3、调用这个方法,出现超时的情况,说明你的并发已经超过了数据库所能处理的极限,数据库无限等待导致超时

    基于上面的分析,建议采用线程池的方案,支付宝的单号就是用的线程池的方案进行的。

    数据库 update 不是一次加1,而是一次加几百甚至上千,然后取到的这 1000个序号,放在线程池里慢慢分配即可,能应付任意大的并发,同时保证数据库没任何压力。


    展开全文
  • JAVA高并发

    2016-12-26 11:29:24
    分享链接: java.util.concurrent包源码分析 ... 聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源码分析 http://blog.csdn.net/ITer_ZC/article/


    分享链接:

    java.util.concurrent包源码分析

    http://www.cnblogs.com/wanly3643/category/437878.html


    聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源码分析

    http://blog.csdn.net/ITer_ZC/article/details/46913841


    Java 并发工具包 java.util.concurrent 用户指南

    http://blog.csdn.net/defonds/article/details/44021605/

    展开全文
  • 实战java高并发程序设计,学习java并发编程的一手资料,值得一看。在过去单核CPU时代,单任务在一个时间点只能执行单一程序,随着多核CPU的发展,并行程序开发就显得尤为重要。, 《实战Java高并发程序设计》主要介绍...
  • JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程
  • Java高并发秒杀API(四)之高并发优化

    万次阅读 多人点赞 2017-10-06 17:07:54
    Java高并发秒杀API(四)之高并发优化1. 高并发优化分析 关于并发 并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS...

    Java高并发秒杀API(四)之高并发优化

    1. 高并发优化分析

    关于并发

    并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS(Query Per Second每秒查询率)和事务执行的时间有密切关系,事务执行时间越短,并发性越高,这也是要将费时的I/O操作移出事务的原因。

    在本项目中高并发发生在哪?

    高并发发生的地方

    在上图中,红色的部分就表示会发生高并发的地方,绿色部分表示对于高并发没有影响。

    为什么需要单独获取系统时间?

    这是为了我们的秒杀系统的优化做铺垫。比如在秒杀还未开始的时候,用户大量刷新秒杀商品详情页面是很正常的情况,这时候秒杀还未开始,大量的请求发送到服务器会造成不必要的负担。

    我们将这个详情页放置到CDN中,这样用户在访问该页面时就不需要访问我们的服务器了,起到了降低服务器压力的作用。而CDN中存储的是静态化的详情页和一些静态资源(css,js等),这样我们就拿不到系统的时间来进行秒杀时段的控制,所以我们需要单独设计一个请求来获取我们服务器的系统时间。

    详情页

    CDN(Content Delivery Network)的理解

    CDN

    获取系统时间不需要优化

    因为Java访问一次内存(Cacheline)大约10ns,1s=10亿ns,也就是如果不考虑GC,这个操作1s可以做1亿次。

    秒杀地址接口分析

    • 无法使用CDN缓存,因为CDN适合请求对应的资源不变化的,比如静态资源、JavaScript;秒杀地址返回的数据是变化的,不适合放在CDN缓存;
    • 适合服务端缓存:Redis等,1秒钟可以承受10万qps。多个Redis组成集群,可以到100w个qps. 所以后端缓存可以用业务系统控制。

    秒杀地址接口优化

    秒杀地址接口优化

    秒杀操作优化分析

    • 无法使用cdn缓存
    • 后端缓存困难: 库存问题
    • 一行数据竞争:热点商品

    大部分写的操作和核心操作无法使用CDN,也不可能在缓存中减库存。你在Redis中减库存,那么用户也可能通过缓存来减库存,这样库存会不一致,所以要通过mysql的事务来保证一致性。

    比如一个热点商品所有人都在抢,那么会在同一时间对数据表中的一行数据进行大量的update set操作。

    行级锁在commit之后才释放,所以优化方向是减少行级锁的持有时间。

    延迟问题很关键

    • 同城机房网络(0.5ms~2ms),最高并发性是1000qps。
    • Update后JVM -GC(垃圾回收机制)大约50ms,最高并发性是20qps。并发性越高,GC就越可能发生,虽然不一定每次都会发生,但一定会发生。
    • 异地机房,比如北京到上海之间的网络延迟,进过计算大概13~20ms。

      网络延迟计算

    如何判断update更新库存成功?

    有两个条件:

    1. update自身没报错;
    2. 客户端确认update影响记录数

    优化思路:

    • 把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响

    如何把客户端逻辑放到MySQL服务端

    有两种方案:

    1. 定制SQL方案,在每次update后都会自动提交,但需要修改MySQL源码,成本很高,不是大公司(BAT等)一般不会使用这种方法。
    2. 使用存储过程:整个事务在MySQL端完成,用存储过程写业务逻辑,服务端负责调用。

    接下来先分析第一种方案

    秒杀方案1

    秒杀方案1成本分析

    根据上图的成本分析,我们的秒杀系统采用第二种方案,即使用存储过程。

    优化总结

    • 前端控制

    暴露接口,按钮防重复(点击一次按钮后就变成灰色,禁止重复点击按钮)

    • 动静态数据分离

    CDN缓存,后端缓存

    • 事务竞争优化

    减少事务行级锁的持有时间

    2. Redis后端缓存优化编码

    关于CDN的说明

    由于不同公司提供的CDN的接口暴露不同,不同的公司租用的机房调用的API也不相同,所以慕课网的视频中并没有对CDN的使用过程进行讲解。

    2.1 下载安装Redis

    前往官网下载安装Stable版本的Redis,安装后可以将安装目录添加到系统变量Path里以方便使用,我使用的是Windows系统的Redis,懒得去官网下载的可以点这里下载

    安装后,运行redis-server.exe启动服务器成功,接着运行redis-cli.exe启动客户端连接服务器成功,说明Redis已经安装成功了。

    为什么使用Redis

    Redis属于NoSQL,即非关系型数据库,它是key-value型数据库,是直接在内存中进行存取数据的,所以有着很高的性能。

    利用Redis可以减轻MySQL服务器的压力,减少了跟数据库服务器的通信次数。秒杀的瓶颈就在于跟数据库服务器的通信速度(MySQL本身的主键查询非常快)

    2.2 在pom.xml中配置Redis客户端

    <!--添加Redis依赖 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.7.3</version>
    </dependency>
    

    Jedis

    Redis有很多客户端,我们的项目是用Java语言写的,自然选择对应Java语言的客户端,而官网最推荐我们的Java客户端是Jedis,在pom.xml里配置了Jedis依赖就可以使用它了,记得要先开启Redis的服务器,Jedis才能连接到服务器。

    由于Jedis并没有实现内部序列化操作,而Java内置的序列化机制性能又不高,我们是一个秒杀系统,需要考虑高并发优化,在这里我们采用开源社区提供的更高性能的自定义序列化工具protostuff。

    2.3 在pom.xml中配置protostuff依赖

    <!--prostuff序列化依赖 -->
    <dependency>
        <groupId>com.dyuproject.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>1.0.8</version>
    </dependency>
    <dependency>
        <groupId>com.dyuproject.protostuff</groupId>
        <artifactId>protostuff-runtime</artifactId>
        <version>1.0.8</version>
    </dependency>
    

    关于序列化和反序列化

    序列化是处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象在网络间传输。反序列化就是将流化后的对象重新转化成原来的对象。

    在Java中内置了序列化机制,通过implements Serializable来标识一个对象实现了序列化接口,不过其性能并不高。

    2.4 使用Redis优化地址暴露接口

    原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接去缓存中直接查询到。

    以上属于数据访问层的逻辑(DAO层),所以我们需要在dao包下新建一个cache目录,在该目录下新建RedisDao.java,用来存取缓存。

    RedisDao

    public class RedisDao {
        private final JedisPool jedisPool;
    
        public RedisDao(String ip, int port) {
            jedisPool = new JedisPool(ip, port);
        }
    
        private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
    
        public Seckill getSeckill(long seckillId) {
            // redis操作逻辑
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckillId;
                    // 并没有实现哪部序列化操作
                    // 采用自定义序列化
                    // protostuff: pojo.
                    byte[] bytes = jedis.get(key.getBytes());
                    // 缓存重获取到
                    if (bytes != null) {
                        Seckill seckill = schema.newMessage();
                        ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                        // seckill被反序列化
    
                        return seckill;
                    }
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
            return null;
        }
    
        public String putSeckill(Seckill seckill) {
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckill.getSeckillId();
                    byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                            LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                    // 超时缓存
                    int timeout = 60 * 60;// 1小时
                    String result = jedis.setex(key.getBytes(), timeout, bytes);
    
                    return result;
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
    
            return null;
        }
    }
    

    注意

    使用protostuff序列化工具时,被序列化的对象必须是pojo对象(具备setter/getter)

    在spring-dao.xml中手动注入RedisDao

    由于RedisDao和MyBatis的DAO没有关系,MyBatis不会帮我们自动实现该接口,所以我们需要在spring-dao.xml中手动注入RedisDao。由于我们在RedisDao是通过构造方法来注入ip和port两个参数的,所以需要配置,如果不配置这个标签,我们需要为ip和port提供各自的setter和getter(注入时可以没有getter)。

    在这里我们直接把value的值写死在标签里边了,实际开发中需要把ip和port参数的值写到配置文件里,通过读取配置文件的方式读取它们的值。

    <!--redisDao -->
    <bean id="redisDao" class="com.lewis.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost" />
        <constructor-arg index="1" value="6379" />
    </bean>
    

    修改SeckillServiceImpl

    使用注解注入RedisDao属性

     @Autowired
     private RedisDao redisDao;
    

    修改exportSeckillURI()

    public Exposer exportSeckillUrl(long seckillId) {
        // 优化点:缓存优化:超时的基础上维护一致性
        // 1.访问redis
    
        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            // 2.访问数据库
            seckill = seckillDao.queryById(seckillId);
            if (seckill == null) {// 说明查不到这个秒杀产品的记录
                return new Exposer(false, seckillId);
            } else {
                // 3.放入redis
                redisDao.putSeckill(seckill);
            }
        }
    
        // 若是秒杀未开启
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        // 系统当前时间
        Date nowTime = new Date();
        if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
    
        // 秒杀开启,返回秒杀商品的id、用给接口加密的md5
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }
    

    2.5 测试类RedisDaoTest

    通过IDE工具快速生成测试类RedisDaoTest,新写一个testSeckill(),对getSeckill和putSeckill方法进行全局测试。

    @RunWith(SpringJUnit4ClassRunner.class)
    // 告诉junit spring的配置文件
    @ContextConfiguration({ "classpath:spring/spring-dao.xml" })
    public class RedisDaoTest {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private long id = 1001;
    
        @Autowired
        private RedisDao redisDao;
    
        @Autowired
        private SeckillDao seckillDao;
    
        @Test
        public void testSeckill() {
    
            Seckill seckill = redisDao.getSeckill(id);
            if (seckill == null) {
                seckill = seckillDao.queryById(id);
                if (seckill != null) {
                    String result = redisDao.putSeckill(seckill);
                    logger.info("result={}", result);
                    seckill = redisDao.getSeckill(id);
                    logger.info("seckill={}", seckill);
                }
            }
        }
    
    }
    

    如果测试通过了,会输出result={}OK以及id为1001的商品信息,如果输出的都是null,那说明你没有开启Redis服务器,所以在内存中没有存取到缓存。

    为什么不用Redis的hash来存储对象?

    第一:通过Jedis储存对象的方式有大概三种

    1. 本项目采用的方式:将对象序列化成byte字节,最终存byte字节;
    2. 对象转hashmap,也就是你想表达的hash的形式,最终存map;
    3. 对象转json,最终存json,其实也就是字符串

    第二:其实如果你是平常的项目,并发不高,三个选择都可以,这种情况下以hash的形式更加灵活,可以对象的单个属性,但是问题来了,在秒杀的场景下,三者的效率差别很大。

    第三:结果如下

    10w数据 时间 内存占用
    存json 10s 14M
    存byte 6s 6M
    存jsonMap 10s 20M
    存byteMap 4s 4M
    取json 7s
    取byte 4s
    取jsonmap 7s
    取bytemap 4s

    第四:你该说了,bytemap最快啊,为啥不用啊,因为项目用了超级强悍的序列化工具啊,以上测试是基于java的序列化,如果改了序列化工具,你可以测试下。

    以上问答源自慕课网的一道问答

    教学视频中张老师对于Redis暴露接口地址的补充

    1. redis事务与RDBMS事务有本质区别,详情见http://redis.io/topics/transactions
    2. 关于spring整合redis。原生Jedis API已经足够清晰。笔者所在的团队不使用任何spring-data整合API,而是直接对接原生Client并做二次开发调优,如Jedis,Hbase等。
    3. 这里使用redis缓存方法用于暴露秒杀地址场景,该方法存在瞬时压力,为了降低DB的primary key QPS,且没有使用库存字段所以不做一致性维护。
    4. 跨数据源的严格一致性需要2PC支持,性能不尽如人意。线上产品一般使用最终一致性去解决,这块相关知识较多,所以没有讲。
    5. 本课程的重点其实不是SSM,只是一个快速开发的方式。重点根据业务场景分析通信成本,瓶颈点的过程和优化思路。
    6. 初学者不要纠结于事务。事务可以降低一致性维护难度,但扩展性灵活性存在不足。技术是死的,人是活的。比如京东抢购使用Redis+LUA+MQ方案,就是一种技术反思。

    3. 秒杀操作——并发优化

    3.1 简单优化

    回顾事务执行

    回顾事务执行

    sql语句的简单优化

    简单优化

    优化SeckillServiceImpl的executeSeckill()

    用户的秒杀操作分为两步:减库存、插入购买明细,我们在这里进行简单的优化,就是将原本先update(减库存)再进行insert(插入购买明细)的步骤改成:先insert再update。

    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException,
            RepeatKillException, SeckillCloseException {
    
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            throw new SeckillException("seckill data rewrite");// 秒杀数据被重写了
        }
        // 执行秒杀逻辑:减库存+增加购买明细
        Date nowTime = new Date();
    
        try {
    
            // 否则更新了库存,秒杀成功,增加明细
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            // 看是否该明细被重复插入,即用户是否重复秒杀
            if (insertCount <= 0) {
                throw new RepeatKillException("seckill repeated");
            } else {
    
                // 减库存,热点商品竞争
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    // 没有更新库存记录,说明秒杀结束 rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    // 秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            // 将编译期异常转化为运行期异常
            throw new SeckillException("seckill inner error :" + e.getMessage());
        }
    
    }
    

    为什么要先insert再update

    首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。

    在这里,插入是可以并行的,而更新由于会加行级锁是串行的。

    也就是说是更新在前加锁和释放锁之间两次的网络延迟和GC,如果插入在前则加锁和释放锁之间只有一次的网络延迟和GC,也就是减少的持有锁的时间。

    这里先insert并不是忽略了库存不足的情况,而是因为insert和update是在同一个事务里,光是insert并不一定会提交,只有在update成功才会提交,所以并不会造成过量插入秒杀成功记录。

    3.2 深度优化

    前边通过调整insert和update的执行顺序来实现简单优化,但依然存在着Java客户端和服务器通信时的网络延迟和GC影响,我们可以将执行秒杀操作时的insert和update放到MySQL服务端的存储过程里,而Java客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的GC影响。另外,由于我们使用了存储过程,也就使用不到Spring的事务管理了,因为在存储过程里我们会直接启用一个事务。

    3.2.1 写一个存储过程procedure,然后在MySQL控制台里执行它

    -- 秒杀执行储存过程
    DELIMITER $$ -- 将定界符从;转换为$$
    -- 定义储存过程
    -- 参数: in输入参数   out输出参数
    -- row_count() 返回上一条修改类型sql(delete,insert,update)的影响行数
    -- row_count:0:未修改数据 ; >0:表示修改的行数; <0:sql错误
    CREATE PROCEDURE `seckill`.`execute_seckill`
      (IN v_seckill_id BIGINT, IN v_phone BIGINT,
       IN v_kill_time  TIMESTAMP, OUT r_result INT)
      BEGIN
        DECLARE insert_count INT DEFAULT 0;
        START TRANSACTION;
        INSERT IGNORE INTO success_killed
        (seckill_id, user_phone, state)
        VALUES (v_seckill_id, v_phone, 0);
        SELECT row_count() INTO insert_count;
        IF (insert_count = 0) THEN
          ROLLBACK;
          SET r_result = -1;
        ELSEIF (insert_count < 0) THEN
            ROLLBACK;
            SET r_result = -2;
        ELSE
          UPDATE seckill
          SET number = number - 1
          WHERE seckill_id = v_seckill_id
                AND end_time > v_kill_time
                AND start_time < v_kill_time
                AND number > 0;
          SELECT row_count() INTO insert_count;
          IF (insert_count = 0) THEN
            ROLLBACK;
            SET r_result = 0;
          ELSEIF (insert_count < 0) THEN
              ROLLBACK;
              SET r_result = -2;
          ELSE
            COMMIT;
            SET r_result = 1;
          END IF;
        END IF;
      END;
    $$
    -- 储存过程定义结束
    -- 将定界符重新改为;
    DELIMITER ;
    
    -- 定义一个用户变量r_result
    SET @r_result = -3;
    -- 执行储存过程
    CALL execute_seckill(1003, 13502178891, now(), @r_result);
    -- 获取结果
    SELECT @r_result;
    

    注意点

    CREATE PROCEDURE `seckill`.`execute_seckill`
    

    上边这句语句的意思是为一个名为seckill的数据库定义一个名为execute_seckill的存储过程,如果你在连接数据库后使用了这个数据库(即use seckill;),那么这里的定义句子就不能这样写了,会报错(因为存储过程是依赖于数据库的),改成下边这样:

    CREATE PROCEDURE `execute_seckill`
    

    row_count()

    存储过程中,row_count()函数用来返回上一条sql(delete,insert,update)影响的行数。

    根据row_count()返回值,可以进行接下来的流程判断:

    0:未修改数据;

    >0: 表示修改的行数;

    <0: 表示SQL错误或未执行修改SQL

    3.2.2 修改源码以调用存储过程

    SeckillDao里添加调用存储过程的方法声明

    /**
     *  使用储存过程执行秒杀
     * @param paramMap
     */
    void killByProcedure(Map<String,Object> paramMap);
    

    接着在SeckillDao.xml里添加该方法对应的sql语句

    <!--调用储存过程 -->
    <select id="killByProcedure" statementType="CALLABLE">
        CALL execute_seckill(
            #{seckillId,jdbcType=BIGINT,mode=IN},
            #{phone,jdbcType=BIGINT,mode=IN},
            #{killTime,jdbcType=TIMESTAMP,mode=IN},
            #{result,jdbcType=INTEGER,mode=OUT}
        )
    </select>
    

    SeckillService接口里添加一个方法声明

    /**
     * 调用存储过程来执行秒杀操作,不需要抛出异常
     * 
     * @param seckillId 秒杀的商品ID
     * @param userPhone 手机号码
     * @param md5 md5加密值
     * @return 根据不同的结果返回不同的实体信息
     */
    SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5);
    

    为什么这个方法不需要抛出异常?

    原本没有调用存储过程的执行秒杀操作之所以要抛出RuntimException,是为了让Spring事务管理器能够在秒杀不成功的时候进行回滚操作。而现在我们使用了存储过程,有关事务的提交或回滚已经在procedure里完成了,前面也解释了不需要再使用到Spring的事务了,既然如此,我们也就不需要在这个方法里抛出异常来让Spring帮我们回滚了。

    SeckillServiceImpl里实现这个方法

    我们需要使用到第三方工具类,所以在pom.xml里导入commons-collections工具类

    <!--导入apache工具类-->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.2</version>
    </dependency>
    

    在接口的实现类里对executeSeckillProcedure进行实现

    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);
        }
        Date killTime = new Date();
        Map<String, Object> map = new HashMap<>();
        map.put("seckillId", seckillId);
        map.put("phone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);
        // 执行储存过程,result被复制
        seckillDao.killByProcedure(map);
        // 获取result
        int result = MapUtils.getInteger(map, "result", -2);
        if (result == 1) {
            SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
            return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
        } else {
            return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
        }
    }
    

    接着对该方法进行测试,在原本的SeckillServiceTest测试类里添加测试方法

    @Test
    public void executeSeckillProcedure(){
        long seckillId = 1001;
        long phone = 13680115101L;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed()) {
            String md5 = exposer.getMd5();
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
            logger.info("execution={}", execution);
        }
    }
    

    经过测试,发现没有问题,测试通过。然后我们需要把Controller里的执行秒杀操作改成调用存储过程的方法。

        @RequestMapping(value = "/{seckillId}/{md5}/execution",
                method = RequestMethod.POST,
                produces = {"application/json;charset=UTF-8"})
        @ResponseBody
        public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
                                                       @PathVariable("md5") String md5,
                                                       @CookieValue(value = "userPhone",required = false) Long userPhone)
        {
            if (userPhone==null)
            {
                return new SeckillResult<SeckillExecution>(false,"未注册");
            }
    
            try {
                //这里改为调用存储过程
    //            SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
                SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);
                return new SeckillResult<SeckillExecution>(true, execution);
            }catch (RepeatKillException e1)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
                return new SeckillResult<SeckillExecution>(true,execution);
            }catch (SeckillCloseException e2)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
            catch (Exception e)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
        }
    

    存储过程优化总结

    1. 存储过程优化:事务行级锁持有的时间
    2. 不要过度依赖存储过程
    3. 简单的逻辑依赖存储过程
    4. QPS:一个秒杀单6000/qps

    经过简单优化和深度优化之后,本项目大概能达到一个秒杀单6000qps(慕课网视频中张老师说的),这个数据对于一个秒杀商品来说其实已经挺ok了,注意这里是指同一个秒杀商品6000qps,如果是不同商品不存在行级锁竞争的问题。

    3.3 系统部署架构

    系统可能用到的服务

    CDN:放置一些静态化资源,或者可以将动态数据分离。一些js依赖直接用公网的CDN,自己开发的一些页面也做静态化处理推送到CDN。用户在CDN获取到的数据不需要再访问我们的服务器,动静态分离可以降低服务器请求量。比如秒杀详情页,做成HTML放在cdn上,动态数据可以通过ajax请求后台获取。

    Nginx:作为http服务器,响应客户请求,为后端的servlet容器做反向代理,以达到负载均衡的效果。

    Redis:用来做服务器端的缓存,通过Jedis提供的API来达到热点数据的一个快速存取的过程,减少数据库的请求量。

    MySQL:保证秒杀过程的数据一致性与完整性。

    智能DNS解析+智能CDN加速+Nginx并发+Redis缓存+MySQL分库分表

    大型系统部署架构

    大型系统部署架构,逻辑集群就是开发的部分。

    1. Nginx做负载均衡
    2. 分库分表:在秒杀系统中,一般通过关键的秒杀商品id取模进行分库分表,以512为一张表,1024为一张表。分库分表一般采用开源架构,如阿里巴巴的tddl分库分表框架。
    3. 统计分析:一般使用hadoop等架构进行分析

    在这样一个架构中,可能参与的角色如下:

    项目角色

    本节结语

    至此,关于该SSM实战项目——Java高并发秒杀API已经全部完成,感谢观看本文。

    项目笔记相关链接

    项目源码

    项目视频教程链接

    这是慕课网上的一个免费项目教学视频,名为Java高并发秒杀API,一共有如下四节课程,附带视频传送门(在视频中老师是用IDEA,本文用的是Eclipse)

    高并发的相关推荐

    1. java系统高并发解决方案(转载)
    展开全文
  • Java高并发系列第二讲 并发级别

    万次阅读 2020-09-01 20:02:17
    由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别分为阻塞、无饥饿、无障碍、无锁、无等待几种。 阻塞 一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续...
  • 高并发复用数据库链接技术详解之数据库连接池 类加载器的高级特性(自定义类加器实现加密解密) iBATIS开源主流框架(实现半自动化hibernate) 企业实用技能之详解(眼睛横纹模式验证码防止恶意登陆) 动态页面的静态化...
  • java 高并发面试题

    万次阅读 多人点赞 2018-02-28 21:43:18
    1、线程与进程 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;...
  • Java 高并发思路

    2019-02-28 21:13:14
    此文记录了自己学习Java 高并发思路的基础知识,纪念自己面试前的准备。 扩容 垂直扩容(纵向扩容):提高系统部件能力 水平扩展(横向扩容):增加更多系统成员 读操作扩展 memcache、redis、CDN 写操作扩展 ...
  • 实践java高并发编程

    2018-06-22 09:31:10
    系统的讲解了java高并发的实现,建立高可用的应用程序。
  • Java高并发解决方案

    万次阅读 多人点赞 2019-03-03 22:23:50
    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。...在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个W...
  • 此课程是对java高并发编程原理及源码的深度解析,课程内容为Java对象头的布局 java对象头的组成部分 JOL的的使用 什么是小端存储莫 mark word的介绍 mark word的hash存储
  • 龙果JAVA高并发实战

    2018-05-17 23:32:54
    帮助你构建完整的并发与高并发知识体系,胜任实际开发中并发与高并发问题的处理,倍增高薪面试成功率!
  • java高并发:CAS无锁原理及广泛应用

    万次阅读 多人点赞 2016-12-20 12:53:39
    应对高并发需要在各个技术层面进行合理的设计和技术选型才可以。本文只讲述微观层面是如何应对多线程高并发的,介绍著名的CAS原理以及其广泛应用。 本文中jdk版本使用的是jdk1.7.0_55. 不同版本实现可能稍有差异. ...
  • java高并发,编程,技术宝典。java高并发,编程,技术宝典。java高并发,编程,技术宝典。java高并发,编程,技术宝典。java高并发,编程,技术宝典。
  • java高并发解决方案

    2015-06-24 15:52:32
    缓存一词搞技术的都接触过,很多地方用到缓存。网站架构和网站开发中的缓存也是非常重要。这里先讲述最基本的两种缓存。高级和分布式的缓存在后面讲述。
  • java高并发处理方法

    2018-09-07 17:17:35
    总共有6小节介绍解决方案,个人觉得是挺有用的
  • java高并发秒杀api源码

    热门讨论 2016-11-29 23:01:35
    java高并发秒杀api源码
  • JAVA高并发编程

    万次阅读 多人点赞 2018-05-07 09:30:26
    synchronized 关键字 同步方法 同步代码块 锁的底层实现 锁的种类 volatile 关键字 wait¬ify AtomicXxx 类型组 CountDownLatch 门闩 锁的重入 ReentrantLock 同步容器 Map/Set List Queue ......
  • java高并发程序设计视频全集,并发场景,死锁,活锁,阻塞,非阻塞...
  • java 高并发 视频教程 架构级别 链接:https://pan.baidu.com/s/1pPG6rB-fxkEPRtZUv3QOSw 密码:见文件
  • 实战Java高并发程序设计主要介绍基于Java的并行程序设计基础、思路、方法和实战。第一,立足于并发程序基础,详细介绍Java中进行并行程序设计的基本方法。第二,进一步详细介绍JDK中对并行程序的强大支持,帮助读者...
  • 对于我们所研发的网站,若网站的访问量非常大,那么我们必须考虑相关的并发访问问题,而并发问题是绝大部分的程序员头疼的问题。 本 Chat 带你领略一下相关概念和解决方案。本 Chat 文章部分观点来自网站整理,介意...
  • Java高并发秒杀——高并发优化 高并发优化:由于该系统还是存在高并发问题,并发访问量大概只有100左右,可以通过高并发优化将并发量提高到5000左右 高并发优化分析目录   1、高并发优化分析   2、redis后端缓存...

空空如也

1 2 3 4 5 ... 20
收藏数 21,398
精华内容 8,559
热门标签
关键字:

java高并发

java 订阅