日期:2014-05-19  浏览次数:20728 次

J2EE事务并发控制策略总结
本文结合Hibernate以及JPA标准,对J2EE当前持久层设计所遇到的几个问题进行总结:

    事务并发访问控制策略

    当前J2EE项目中,面临的一个共同问题就是如果控制事务的并发访问,虽然有些持久层框架已经为我们做了很多工作,但是理解原理,对于我们开发来说还是很有用处的。

    事务并发访问主要可以分为两类,分别是同一个系统事务和跨事务访问的并发访问控制,其中同一个系统事务可以采取乐观锁以及悲观锁策略,而跨多个系统事务时则需要乐观离线锁和悲观离线锁。在讨论这四种并发访问控制策略之前,先需要明确一下数据库事务隔离级别的问题,ANSI标准规定了四个数据库事务隔离级别,它们分别是:

    读取未提交(Read Uncommitted)

    这是最低的事务隔离级别,读事务不会阻塞读事务和写事务,写事务也不会阻塞读事务,但是会阻塞写事务。这样造成的一个结果就是当一个写事务没有提交的时候,读事务照样可以读取,那么造成了脏读的现象。

    读取已提交(Read Committed)

    采用此种隔离界别的时候,写事务就会阻塞读事务和写事务,但是读事务不会阻塞读事务和写事务,这样因为写事务会阻塞读取事务,那么从而读取事务就不能读到脏数据,但是因为读事务不会阻塞其它的事务,这样还是会造成不可重复读的问题。

    可重复读(Repeatable Read)

    采用此种隔离级别,读事务会阻塞写事务,但是读事务不会阻塞读事务,但是写事务会阻塞写事务和读事务。因为读事务阻塞了写事务,这样以来就不会造成不可重复读的问题,但是这样还是不能避免幻影读问题。

    序列化(serializable)

    此种隔离级别是最严格的隔离级别,如果设置成这个级别,那么就不会出现以上所有的问题(脏读,不可重复读,幻影读)。但是这样以来会极大的影响到我们系统的性能,因此我们应该避免设置成为这种隔离级别,相反的,我们应该采用较低的隔离界别,然后再采用并发控制策略来进行事务的并发访问控制)。

    其实我们也可以把事务隔离级别设置为serializable,这样就不需要采用并发控制策略了,数据库就会为我们做好一切并发控制,但是这样以来会严重影响我们系统的伸缩性和性能,所以在实践中,我们一般采用读取已提交或者更低的事务隔离级别,配合各种并发访问控制策略来达到并发事务控制的目的。下面总结一下常用的控制策略:

    1 乐观锁

    乐观锁是在同一个数据库事务中我们常采取的策略,因为它能使得我们的系统保持高的性能的情况下,提高很好的并发访问控制。乐观锁,顾名思义就是保持一种乐观的态度,我们认为系统中的事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个事务更新时,我们想看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败,。

    最后我们需要明确一个问题,因为乐观锁其实并不会锁定任何记录,所以如果我们数据库的事务隔离级别设置为读取已提交或者更低的隔离界别,那么是不能避免不可重复读问题的(因为此时读事务不会阻塞其它事务),所以采用乐观锁的时候,系统应该要容许不可重复读问题的出现。

    了解了乐观锁的概念以后,那么当前我们系统中又是如何来使用这种策略的呢?一般可以采用以下三种方法:

    版本(Version)字段:在我们的实体中增加一个版本控制字段,每次事务更新后就将版本字段的值加1.

    时间戳(timestamps):采取这种策略后,当每次要提交更新的时候就会将系统当前时间和实体加载时的时间进行比较,如果不一致,那么就报告乐观锁失败,从而回滚事务或者重新尝试提交。采用时间戳有一些不足,比如在集群环境下,每个节点的时间同步也许会成问题,并且如果并发事务间隔时间小于当前平台最小的时钟单位,那么就会发生覆盖前一个事务结果的问题。因此一般采用版本字段比较好。

    基于所有属性进行检测:采用这种策略的时候,需要比较每个字段在读取以后有没有被修改过,所以这种策略实现起来比较麻烦,要求对每个属性都进行比较,如果采用hiernate的话,因为Hibernate在一级缓存中可以进行脏检测,那么可以判断哪些字段被修改过,从而动态的生成sql语句进行更新。

    下面再总结一下如何在JDBC和Hibernate中使用乐观锁:

JDBC中使用乐观锁:如果我们采用JDBC来实现持久层的话,那么就可以采用以上将的三种支持乐观锁的策略,在实体中增加一个version字段或者一个Date字段,也可以采用基于所有属性的策略,下面就采用version字段来做一演示:

    假如系统中有一个Account的实体类,我们在Account中多加一个version字段,那么我们JDBC Sql语句将如下写:

Select a.version....from Account as a where (where condition..)
Update Account set version = version+1.....(another field) where version =?...(another contidition)




    这样以来我们就可以通过更新结果的行数来进行判断,如果更新结果的行数为0,那么说明实体从加载以来已经被其它事务更改了,所以就抛出自定义的乐观锁定异常(或者也可以采用Spring封装的异常体系)。具体实例如下:

.......
int rowsUpdated = statement.executeUpdate(sql);
If(rowsUpdated= =0){
throws new OptimisticLockingFailureException();
}
........




    在使用JDBC API的情况下,我们需要在每个update语句中,都要进行版本字段的更新以及判断,因此如果稍不小心就会出现版本字段没有更新的问题,相反当前的 ORM框架却为我们做好了一切,我们仅仅需要做的就是在每个实体中都增加version或者是Date字段。

    Hibernate中使用乐观锁:如果我们采用Hibernate做为持久层的框架,那么实现乐观锁将变得非常容易,因为框架会帮我们生成相应的sql语句,不仅减少了开发人员的负担,而且不容易出错。下面同样采用version字段的方式来总结一下:
同样假如系统中有一个Account的实体类,我们在Account中多加一个version字段,

public class Account{
Long id ;
.......
@Version //也可以采用XML文件进行配置
Int version
.......

}




    这样以来每次我们提交事务时,hibernate内部会生成相应的SQL语句将版本字段加1,并且进行相应的版本检测,如果检测到并发乐观锁定异常,那么就抛出StaleObjectStateException.

    2 悲观锁

    所谓悲观锁,顾名思义就是采用一种悲观的态度来对待事务并发问题,我们认为系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样以来,我们就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。