对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了,然而并发问题是令我们大多数程序员头疼的问题,但话又说回来了,既然逃避不掉,那我们就坦然面对吧~今天就让我们深入研究一下常见的并发和同步问题吧。

 

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

    为了更好的理解同步和并发的问题,我们需要先掌握两个重要的概念:同步、异步

    同步:可以理解为在执行完一个函数或者方法后,一直等待系统返回值或消息,这时程序是处于阻塞的状态,只有接收到系统的返回值或者消息后,才会继续往下执行。

    异步:执行完函数或方法后,不必阻塞性的等待返回值或消息,只需要向系统委托一个异步过程,那么系统接收到返回值或消息时,就会自动触发委托的异步过程,从而完成一个完整的流程。

 

    同步在一定程度上可以看做是单线程,这个线程请求一个方法后,就等待这个方法给他回复,否则不往下执行(死心眼子)。

    异步在一定程度上可以看做是多线程(废话,一个线程怎么叫异步),请求一个方法后就不管了,继续执行接下来的其他方法。

    

    同步就一件事、一件事、一件事的做。

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

    例如:吃饭和说话是同步的,只能一件事一件事的来,因为只有一张嘴。吃饭和听音乐是异步的,听音乐不影响我们吃饭。

  

  对于java程序员来说,我们经常见到同步关键字 synchronized,假如这个同步的监视对象是一个类,当一个对象A在访问这个类里面的同步方法,此时另外一个对象B也想访问这个类里面的这个同步方法,就会进入阻塞,只有等待前一个对象执行完该同步方法后当前对象才能够继续执行该方法;这就是同步。

  相反,如果方法前没有同步关键字修饰的话,那么不同的对象就可以在同一时间访问同一个方法,这就是异步。

 

  再补充一下,脏数据和不可重复读的概念:

  1、脏数据

    脏读是指:一个事务正在访问数据,并且对数据进行了修改,而这个修改还有提交到数据库中,这时另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的数据是脏数据(Dirty Data),脏数据所做的操作可能是不正确的。

  2、不可重复读

    不可重复读是指:在一个事务内,多次读同一条数据。这个事务还没有结束时,另外一个事务也访问了该数据,那么,在第一个事务中两次读数据之间,由于第二个事务的修改,导致第一个事务两次读到数据可能不一样。这样就发生了在同一个事务内,两次读到的数据是不一样的,因此称为不可重复读。

 

二、如何处理并发和同步

  今天讲的如何处理并发和同步问题主要是通过锁机制去解决。我们需要明白锁机制有两个层面:

  第一是代码层面,如java中的同步锁,典型的就是同步关键字synchronize(还有Lock等)。 感兴趣的可以参考: 

  第二是数据库层面上,比较典型的就是悲观锁和乐观锁,这里重点研究一下悲观锁(传统的物理锁)和乐观锁,这两个锁:

  1. 悲观锁(Pessimistic Locking)

    悲观锁正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及外部系统的事务)的修改持保守状态,因此,在整个数据处理过程中,将数据处于锁定状态。

    悲观锁的实现,一般是依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则即使在本系统中实现了加锁机制,也无法保证外部系统会修改数据)。

    一个典型的依赖数据库悲观锁调用:select * from account where name='zhangsan' for update;

    整条sql锁定了account表中所有符合检索条件(name='zhangsan')的记录;本次事务提交之前(事务提交会释放事务过程中的锁),外界无法修改这些记录。

 

hibernate的悲观锁,也是基于数据库的锁机制实现的,下面代码实现了对查询记录的加锁:

1 String hqlStr ="from TUser as user where user.name='zhangsan' ";
2 Query query = session.createQuery(hqlStr);
3 query.setLockMode("user",LockMode.UPGRADE); // 加锁
4 List userList = query.list();// 执行查询,获取数

 

query.setLocalMode 对查询语句中特定别名所对应的记录进行加锁(我们对TUser类制定了一个别名“user”),这也就是对多有返回的记录加锁。

  

观察允许其的hibernate,生成的sql语句:这里hibernate通过使用数据库的 for update 子句实现了悲观锁机制

1 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的加锁机制有:

      1、LockMode.NONE:无锁机制

      2、LockMode.WRITE:hibernate 在 insert 和 update 的时候记录会自动获取

      3、LockMode.READ:hibernate在读取记录的时候会自动获取

    以上这三种锁机制一般由heibernate内部使用,如hibernate为保证 update 过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。

      4、LockMode.UPGRADE:利用数据的 for update 子句加锁

      5、LockMode.UPGRADE_NOWAIT:Oracle 的特定实现,利用 Oracle 的 for update nowait 子句实现加锁

    上面这两种锁机制是我们应用层较为常用的,加锁一般通过:Criteria.setLockMode;Query.setLockMode;Session.lock;方法实现。

    需要注意点的是:只有在查询开始之前(也就是hibernate生成sql之前)设置加锁,才会真正通过数据的锁机制进行加锁处理,否则,数据已通过不包含 for update 子句的 select sql 加载进来,所谓数据的加锁也就无从谈起。

    为了更好的理解 select... for update 的锁表过程,我们以 mysql 为例,进行研究

    要测试锁定的状态,可利用 mysql 的 Command Mode,开两个视窗来测试:

表的基本结构如下:

表中内容如下:

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

然后再另外一个窗口执行 update 操作:

等到第一个窗口 commit 后:

至此,悲观锁的机制,有一些感觉了吧,

    需要注意的是 for update 要放到 mysql 的事务中,即 begin 和 commit 之间,否则不起作用。

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

 

  2、乐观锁 (Optimistic Locking):

    相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事物而言,这样的开销是无法承受的。

    如一个金融系统当一个操作员读取用户数,并在读出的数据上进行修改操作(如更改用户账户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从读出数据,修改数据,直到提交数据,甚至还包括操作员中途去煮咖啡的时间),数据库的记录始终处于加锁状态,可以想到,如果面对成百上千个并发,这种情况会导致什么样的后果。乐观锁的机制在一定程度上解决了这个问题。

    乐观锁,大多是基于数据版本(version)记录机制实现。数据版本:即为在数据库增加一个版本标识。在基于数据库表的版本解决方案中,一般是通过在数据库表中增加一个 version 字段来实现。读取数据的时候将此版本号一起读出,之后更新时候将此版本号加一。此时,将提交数据的版本信息与数据库表对应记录的当前本版信息进行对比,如果提交数据的当前本版号大于数据库表当前版本号,则予以更新,否则认为是过期数据。对于上面修改用户信息的例子而言,假设数据库账户信息表中有一个 version 字段,当前值为1,当前账户余额字段 balance 为 100;此时操作员A将此数据读出,并从账户余额扣除 50。在操作员 A 操作过程中,操作员 B 也读去了该用户的信息,并从账户余额中扣除 20。这是操作员 A 完成了修改工作,将数据版本信息加一(version = 2),和账户扣除后的余额(balance = 50),提交到数据库更新,此时由于提交数据版本大于数据库记录的版本号,数据记录被更新 version 更新为 2。接着,操作员 B 完成了操作,也将版本号加一(version = 2),试图提交数据(balance = 80),但此时对比数据库记录版本时候发现,数据库当前本版也为2,不满足“数据版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此操作员 B 的提交被驳回。这样就避免了操作员 B 使用基于 vsersion=1 的旧数据修改的结果覆盖了操作员 A 的操作结果。从例子可以看出,乐观锁的机制避免了长事务中数据库加锁数据库开销(操作员A和操作员B操作过程中都没有对数据库数据加锁),大大提升了大并发下系统整体性能的表现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成在脏数据被更新到数据中,在系统设计阶段,我们应该充分考虑到发送这些情况的可能性,并进行相应的调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是直接将数据库表直接对外公开)。

    Hibernate在其数据访问引擎中内置了乐观锁的实现。如果不考虑外部系统对数据库的更新操作,利用Hibernate 提供的透明化乐观锁实现,将大大提升我们的生产力。

User.hbm.xml : 注意 version 节点必须出现在 ID 节点之后。 

1 <?xml version="1.0"?>
 2 <!DOCTYPE hibernate-mapping PUBLIC
 3         "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
 4         "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
 5  
 6 <hibernate-mapping package="com.ayht.test">
 7     <class name="User"  table="user" optimistic-lock="version" >
 8         <id name="id">
 9             <generator class="native" />
10         </id>
11         <!--version标签必须跟在id标签后面-->
12         <version column="version" name="version"  />
13         <property name="userName"/>
14         <property name="password"/>    
15     </class> 
16 </hibernate-mapping>

 

 这里我们申明了一个 version 属性,用于存放用户的版本信息,保存在User表的 version 中,optimistic-lock 属性有如下取值:

  none :无乐观锁

  version : 通过版本机制实现乐观锁

  dirty : 通过检查发送变动的属性实现乐观锁

  all :通过检查所有属性实现乐观锁

 其中通过 version 实现乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中,目前唯一在数据对象脱离Session 发生修改的情况下依然有效的锁机制。因此一般情况下我们都选择 version 的方式作为 Hibernate 的乐观锁实现机制。