美文网首页技术杂谈05 应用技术@IT·互联网
实战Spring事务传播性与隔离性

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

作者: 阿里加多 | 来源:发表于2017-05-04 09:16 被阅读963次

    阿里巴巴长期招聘Java研发工程师p6,p7,p8等上不封顶级别,有意向的可以发简历给我,注明想去的部门和工作地点:1064454834@qq.com

    欢迎关注微信公众号:技术原始积累 获取更多技术干货

    一、事务传播性

    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语句会等待知道事务一提交或者超时。
    超时:
    screenshot.png

    阿里巴巴长期招聘Java研发工程师p6,p7,p8等上不封顶级别,有意向的可以发简历给我,注明想去的部门和工作地点:1064454834@qq.com

    欢迎关注微信公众号:技术原始积累 获取更多技术干货

    欢迎关注微信公众号:技术原始积累 获取更多技术干货

    image.png

    相关文章

      网友评论

      • 657dc8bfbd65:知乎的答案
        为什么会RR会导致幻读
        users: id 主键
        1、T1:select * from users where id = 1;
        2、T2:insert into `users`(`id`, `name`) values (1, 'big cat');
        3、T1:insert into `users`(`id`, `name`) values (1, 'big cat');

        T1 :主事务,检测表中是否有 id 为 1 的记录,没有则插入,这是我们期望的正常业务逻辑。
        T2 :干扰事务,目的在于扰乱 T1 的正常的事务执行。

        在 RR 隔离级别下,1、2 是会正常执行的,3 则会报错主键冲突,对于 T1 的业务来说是执行失败的,这里 T1 就是发生了幻读,因为T1读取的数据状态并不能支持他的下一步的业务,见鬼了一样。
        在 Serializable 隔离级别下,1 执行时是会隐式的添加 gap 共享锁的,从而 2 会被阻塞,3 会正常执行,对于 T1 来说业务是正确的,成功的扼杀了扰乱业务的T2,对于T1来说他读取的状态是可以拿来支持业务的。

        所以 mysql 的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。
        这里要灵活的理解读取的意思,第一次select是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
        不可重复读侧重表达 读-读,幻读则是说 读-写
        maxam0128:InnoDB 在RR级别下通过mvcc避免了幻读吧,上面这个例子T1事务的如果改成select * from users where id = 1 for update;T2应该就影响不了
        阿里加多:@_Tango__ 666
      • 小鱼嘻嘻:rr不存在幻读是因为read view,不同的隔离级别可见性不一样
      • weigs:写的真好,以前一直对这方面很迷惑,看了你的文章豁然开朗😀😀😀
      • saad:事务的传播性一般在事务嵌套时候使用,这句话的意思是不是service调用service的时候?求介绍一下,万分感谢。
        saad:thx,以前一直误解了……:cry:
        阿里加多:@saad 比如methodA 和methodB 都是有事务切面的,那么在methodA内部调用methodB时候就是事务嵌套了
      • maxam0128:感谢楼主详细的分享,1.4中test隔离级别是PROPAGATION_REQUIRED,sayHello隔离级别是PROPAGATION_SUPPORTS的情况,这里应该是传播特性,不是隔离级别。
        阿里加多:@maxam0128 哦,是的,感谢指正

      本文标题:实战Spring事务传播性与隔离性

      本文链接:https://www.haomeiwen.com/subject/dddrtxtx.html