之前写的一个系统在测试环境运行了两周都基本正常,但部署到线上运行了几天以后开始持续报唯一键重复的错误。那段代码很简单,就是先检查B表数据是否存在,如果不存在则往A表插数据,再往B表插数据。显然错误就是A表的数据已经插入了,但是B表的数据居然没插入。

    代码逻辑肯定没有问题,这个现象显然就是整个这个方法的原子性被破坏了,曾经有一次插入了A表,结果B表没插入,但是A表插入的结果没回滚。经过几处数据核对都与这个猜测是一致的。仔细检查了一下事务配置,并找到一些相关资料学习了下,果然事务配的有严重问题。赶紧把学到的东西记录下来,加深一下理解。

    Spring的事务类型一共有7个方式,分别是:
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

    其中,REQUIRED是默认的方式,当使用tx标签的时候,写成propagation="REQUIRED"即可。

    网上对于这些方式的区别都有说明,我自己写了一些测试代码去确认了一下我的理解,在嵌套中区别明显。用一个简单的场景,方法A去调用方法B,这里我举几个例子:

    1. A、B都是REQUIRED,其实两个方法就是合在一个事务里面。那么不论A或B执行的失败,则A、B都会产生回滚。

    2. A是REQUIRED,B是REQUIRES_NEW。B方法在进入的时候有自己的savepoint,退出的时候有自己的commit, 那么B方法就是一个独立的事务。如果方法B执行且成功或失败,都与方法A无关。
    (注意:这里要显示的去catch B方法的exception,否则异常上抛会造成A方法也失败了)

     3. A是REQUIRED,B是NESTED。B方法进入的时候也有自己的savepoint,但是提交的时候是与A方法结束时一起提交,那么B方法执行失败就影响到自己的事务。B执行失败,A依然可以提交。但是A执行失败,B会被回滚。
    (注意:同样要catch B方法的exception)

    非事务就不举例子了,很好理解。知道上面这几种组合,基本就可以应对大部分情况了。但知道事务配置,还需要理解事务的隔离级别,否则读出来数据与预期有出入也会造成问题。当然大部分情况下默认配置就可以满足需求。

    事务隔离级别有以下4种:

1、Serializable:最严格的级别,事务串行执行,资源消耗最大;
2、REPEATABLE READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
3、READ COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
4、Read Uncommitted:保证了读取过程中不会读取到非法数据。

参考资料:

1. Spring声明式事务管理及事务嵌套
http://blog.csdn.net/mini_snow/archive/2009/08/03/4404654.aspx

2. 对Spring事务配置的五种方式的深入研究
http://webservices.ctocio.com.cn/java/321/9055321.shtml

3. spring事务详解
http://yjsun.javaeye.com/blog/148281

4. 浅谈Spring事务隔离级别
http://developer.51cto.com/art/200906/132336.htm