实战Spring事务传播性与隔离性

一、事务传播性

1.1 什么是事务的传播性

事务的传播性一般在事务嵌套时候使用,比如在事务A里面调用了另外一个使用事务的方法,那么这俩个事务是各自作为独立的事务执行提交,还是内层的事务合并到外层的事务一块提交那,这就是事务传播性要确定的问题。下面一一介绍比较常用的事务传播性。

首先奉上事务拦截器TransactionInterceptor事务处理流程图:

拦截器.png

1.2 PROPAGATION_REQUIRED

Spring默认的事务传播机制,如果外层有事务则当前事务加入到外层事务,一块提交一块回滚,如果外层没有事务则当前开启一个新事务。这个机制可以满足大多数业务场景。

试验:

 public class BoA {
        public void test(){
        boB.sayHello();
    }
}

BoA 和boB都是进行过事务增强后的bo,那么在执行test的时候会开启一个事务(或者test调用方已经存在事务则加入该事务),执行到sayHello()时候由于传播性是PROPAGATION_REQUIRED,所以sayHello方法加入到test的事务,那么sayHello和test就会同时提交,同时回滚。值得注意的是如果test里面调用sayHello时候加了trycatch没有把异常跑出去,而sayHello方法却抛出了异常,那么整个事务也会回滚,这时候调用test的外层会受到”Transaction rolled back because it has been marked as rollback-only的异常,而把sayHello真正的异常吃掉了。

平时我们都是在bo里面调用数据库操作,在rpc和screen调用bo,所以bo层不应该catch掉异常,而应该抛出来,在rpc和screen层catch异常。

1.3 PROPAGATION_REQUIRES_NEW

该传播机制是每次新开启一个事务,同时把外层的事务挂起,当前新事务执行完毕后在恢复上层事务的执行。

以上面代码为例,首先进入test方法前会开启一个事务,然后调用sayHello时候会把test的事务挂起,从新开启一个新事务执行sayHello,执行完毕后恢复test的事务。如果sayHello抛出来异常则sayHello的事务会回滚,那么test方法是否回滚那?这个要看情况,如果test在调用sayHello 时候使用了trycatch并且异常没有在catch中throw出来,那么test方法不会回滚,这时候sayHello是提交和回滚对test没有影响,。
如果test中没有加trycatch那么,test也会回滚。

1.4 PROPAGATION_SUPPORTS

该传播机制如果外层有事务则加入该事务,如果不存在也不会创建新事务,直接使用非事务方式执行。

以上面代码为例,由于PROPAGATION_SUPPORTS,所以test和sayHello都没有开启事务,没啥好讲的。

下面看下如果test隔离级别是PROPAGATION_REQUIRED,sayHello隔离级别是PROPAGATION_SUPPORTS的情况。这时候外层test会开启一个事务(或者test调用方已经存在事务则加入该事务),然后sayHello执行时候会加入到test的事务和1.2类似,同时提交同时回滚。

1.5 PROPAGATION_NOT_SUPPORTED

该传播机制不支持事务,如果外层存在事务则挂起外层事务 ,然后执行当前逻辑,执行完毕后,恢复外层事务。

同样这里看下如果test使用PROPAGATION_REQUIRED,sayHello隔离级别是PROPAGATION_NOT_SUPPORTED的情况,首先test会开启一个事务(或者test调用方已经存在事务则加入该事务),然后sayHello执行时候会挂起该事务然后在非事务内做自己的事情,做完后在恢复test的事务。 无论sayHello是否抛出异常,sayHello的事务都不会回滚,因为它不在事务范围内,那test?
这个就和1.3一样了,如果test catch住sayHello的异常没有throw出去,那么test就不回滚,否者回滚。

1.6 PROPAGATION_NEVER

该传播机制不支持事务,如果外层存在事务则直接抛出异常。
IllegalTransactionStateException(
“Existing transaction found for transaction marked with propagation ‘never'”)

1.7 PROPAGATION_MANDATORY

该传播机制是说配置了该传播性的方法只能在已经存在事务的方法中被调用,如果在不存在事务的方法中被调用,会抛出异常。
IllegalTransactionStateException(
“No existing transaction found for transaction marked with propagation ‘mandatory'”);

1.8 PROPAGATION_NESTED

该传播机制特点是可以保存状态保存点,当事务回滚后会回滚到某一个保存点上,从而避免所有嵌套事务都回滚。

上面代码test和sayHello都设置为PROPAGATION_NESTED,如果sayHello抛出异常,两者还是都回滚了,因为sayHello虽然回滚到了savePoint而savepoint里面确实包含了test的操作,但是savepoint后还是会吧异常throw给test,这导致了test的回滚。

总结,只有传播性为PROPAGATION_REQUIRED||PROPAGATION_REQUIRES_NEW||PROPAGATION_NESTED时候才可能开启一个新事务。

二、事务隔离性

2.1 什么是事务的隔离性

事务的隔离性是指多个事务并发执行的时候相互之间不受到彼此的干扰,是事务acid中i,根据隔离程度对隔离性有会分类。在具体介绍事务隔离性前有必要介绍几个名词说明数据库并发操作存在的问题。

2.1.1 脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,具体来说假如有两个事务A和B同时更新一个数据d=1,事务B先执行了select获取到d=1,然后更新d=2但是没有提交,这时候事务A在B没有提交的情况下执行搜索结果d=2,这就是脏读。

2.1.2 不可重复读

所谓不可重复读是指一个事务内在未提交的前提下多次搜索一个数据,搜出来的结果不一致。发生不可重复读的原因是在多次搜索期间这个数据被其他事务更新了。

2.1.3 幻读

所谓幻读是指同一个事务内多次查询(注意查询的sql不一定一样)返回的结果集的不一样(比如新增或者少了一条数据),比如同一个事务A内第一次查询时候有n条记录,但是第二次同等条件下查询却又n+1条记录,这就好像产生了幻觉,为啥两次结果不一样那。其实和不可重复读一样,发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据。不同在于不可重复读是数据内容被修改了,幻读是数据变多了或者少了。

2.2、事务隔离级别

为了解决事务并发带来的问题,才有了sql规范中的四个事务隔离级别,不同隔离级别对上面三个问题部分或者全部做了避免。注意:下面试验用的两个终端都是同时执行了begin为了模拟事务并发。

2.2.1 Read Uncommitted

读未提交隔离级别,就是指一个事务中可以读取其他事务未提交的数据,这个级别会导致脏读。
本文都是以mysql为例引擎InnoDB,mysql默认事务隔离级别为Repeatable_Read:如图:

screenshot.png

下面我们更改隔离级别为Read Uncommitted :

screenshot.png

试验:
下面我们打开两个mysql终端,并且关闭自动提交.
终端一:

screenshot.png

终端二:

screenshot.png

终端一我们开启了一个事务,并且插入了一条数据但是没有提交事务,但是终端二却查询出来了。

终端一执行rollback:

screenshot.png

终端二搜索:

screenshot.png

在终端1回滚后,终端二有搜不到了,所以有可能在终端一没有回滚时候终端二已经获取并使用终端一的数据,而终端一回滚后,数据已经被使用过了,所以导致了脏读。

总结:该隔离级别会导致 脏读,不可重复读,幻读,是最低级的隔离级别,一般不用的。

2.2.2 Read Committed

读已提交隔离级别,一个事务只能读取到其他事务已经提交的数据,可能导致同一个事务中多次搜查结果不一样。

试验:修改事务隔离级别为Read Committed,

screenshot.png

终端一:

screenshot.png

终端二:

screenshot.png

由于终端一执行后没有commit,所以终端二查询不到。

下面终端一执行commit:

screenshot.png

终端二再次执行查询:

screenshot.png

终端一提交后,终端二就可以搜查出来了。

总结:该隔离级别会导致不可重复读和幻读,避免了脏读,oracle默认是该隔离级别。实际项目使用mybaits时候虽然隔离级别是read committed,但是在一个事务中多次搜索还是会是同一个结果,这是因为mybatis一级缓存的原因

2.2.3 Repeatable Read

可重复读隔离级别,一个事务内多次查询数据时候查询的数据内容和第一次查询的一致也就是说第一次查询出来的数据没有被修改,而不管其他事务有没有对这些数据新修改。但是可能其他事务新增一条数据,导致一个事务内查询的结果集里面多了一条记录。mysql默认隔离级别就是这个。
试验:
首先修改事务隔离级别为可重复读:

screenshot.png

模拟修改数据情况:
终端一:

screenshot.png

终端二:

screenshot.png

可以知道终端一已经提交的数据在终端二的事务中还是查不到(注意终端二执行begin要在终端一执行commit前,因为我们要模拟并发事务)。

下面在模拟下新增数据情况
终端一:

screenshot.png

终端二:

screenshot.png

终端一插入了一条记录并且提交,但是终端二还是查询不到新增的记录
总结:这里有点奇怪,按照其他资料显示该隔离级别应该是避免了 脏读,不可重复读,但是还存在幻读,但是试验表明不存在幻读

2.2.4 Serializable

串行化隔离级别,就是多个事务串行化一个个按照顺序执行,这种不存在并发情况,所以可以避免所有事务并发问题。

终端一:

screenshot.png

终端二:

screenshot.png

可以看到终端一打开一个事务后,事务二的insert语句会等待知道事务一提交或者超时。
超时:

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 实战Spring事务传播性与隔离性

  • Trackback 关闭
  • 评论 (6)
    • zhoumuan
    • 2017/10/27 10:14上午

    楼主数据库的Repeatable Read并发事务隔离级别可以避免脏读,不可重复读,但是避免不了幻读

      • hello_world
      • 2017/11/15 9:11上午

      mysql如果使用的是InnoDB引擎(默认),设置的是Repeatable Read级别,就可以避免幻读,因为使用了MVCC

    • casxter
    • 2017/10/30 4:05下午

    楼主 可重复读 无法解决幻读,高性能mysql 介绍里有说

    • hello_world
    • 2017/11/15 9:14上午

    楼主的mysql如果使用InnoDB引擎,设置的是Repeatable Read级别,是可以避免幻读的,可以看下MVCC-多版本并发控制

    • mymy1026
    • 2017/12/13 5:24下午

    楼主咨询个问题,spring默认是jdk动态代理,是不是意味着默认情况下 不开启事务的service方法调用另一个开启事务的service,最终是无事务的?或者说事务的传播机制在这种情况下是不起作用的呢?

      • 加多
      • 2017/12/14 11:38上午

      只有配置了事务拦截器(TransactionInterceptor)事务嵌套策略才会有效,具体可参见:http://ifeve.com/spring%E4%BA%8B%E5%8A%A1%E9%85%8D%E7%BD%AE%E8%A7%A3%E6%83%91/

return top