Spring事务隔离级别,实用小技巧

193 阅读8分钟

Spring事务定义

我正在参与掘金新人创作活动,一起开启写作之路。

如果对事务原理不清楚的,建议先看Spring事务原理

如果对事务传播机制不理解的,建议先看Spring事务传播机制

如果都明白了,那就入正文


Spring事务属性

我们知道@Trasnsactional注解可以设置相关属性,那么我们来看看里面的属性到底有什么作用,propagation之前讲过就不再过多介绍

| 属性 | 必须值? | 默认值| 描述| | --- | --- || --- | --- | | propagation | 是 | REQUIRED | 事务传播行为 | | isolation | 否 | DEFAULT | 事务隔离级别仅对REQUIRED和REQUIRED _new有效果 | | timeout | 否 | -1 | 超时时间仅对REQUIRED和REQUIRED _new有效果 | | read-only | 否 | false | 读写与只读事务仅对REQUIRED和REQUIRED _new有效果 | | rollback-for | 否 | REQUIRED | 设置触发回滚的异常如Exception | | no-rollback-for | 否 | REQUIRED | 设置不回滚的异常 |


isolation

隔离级别脏读不可重复读幻读
Read uncommitted
Read committed×
Repeatable read××
Serializable×××

上表为Spring支持的数据库隔离级别,不同的隔离级别可以解决不同的事务问题,并发程度由上向下逐渐减弱,事务安全性由上向下逐渐增强,接下来我们分析一下事务场景

1.Read uncommitted

读未提交即一个事务读到另外一个事务未提交的数据,会产生脏数据

2.Read committed

读已提交因为读到的是另外一个事务已提交的数据,所以不会产生脏数据,但是会产生不可重复读,即一个事务查询数据A,另外一个事务修改提交了数据A,然后再查数据A就不再是之前的数据了,如下,insertTest()和updateTest()都是新的事务,在insertUsr一个事务方法中对数据test查询了两次,但是结果发生了变化

image.png

image.png

3.Repeatable read

可重复读,这个级别可以避免不可重复读,相当于只要这个事务对数据进行读取或修改,此事务还未提交完成,其他事务不能对这块数据进行其他修改操作,但无法避免幻读,幻读即是一个事务读到另外一个事务提交的新插入的数据

4.Serializable

序列化隔离级别是最安全的级别,相当于串行化顺序执行,可以避免以上产生的所有问题,但是效率和性能很低,一般都不会使用

需要注意的是,并不是所有数据库都支持以上4种隔离级别,所以要根据不同的数据库进行设置,而最常用的就是Read committed,因为可以避免脏数据,其他问题可以通过代码级别的锁去控制

最后说一下幻读和不可重复读的区别,其实单看概念都很接近,都是在一个事务中查询两次数据得到不同的结果,但是仔细梳理还是有区别的,不可重复读偏重于update和delete操作,是单行数据的操作,如果用了可重复读隔离级别后,第一次读取这些数据后会给数据加上行锁,那么其他事务就无法对这些数据行进行操作,但是还是可以进行新数据的插入操作,所以幻读是偏重于insert操作,是整张表的操作,简单概括就是,避免不可重复读,用行锁就可以,避免幻读需要锁表


timeout

这个参数主要是用来设置事务sql超时时间,但是可能和我们所理解的超时有所不同,可能会有人认为这是整个事务代码里面的sql执行时间,其实并不是,而是事务中最后一次执行sql的超时时间,也就是说,这个****需要怎么去理解呢,我们看一下下面的代码例子

image.png

如图所示,两个事务方法都带了timeout属性,insertUser在调用insertTest之前我让线程休眠5秒钟,那么在调用insertTest的时候是否会超时?结果如下

image.png

竟然没有超时,停顿了5秒后继续执行下去了,那么我们再看下面这个代码,修改的地方是只把休眠的线程放到下一个事务方法内,那么是否会超时呢?结果如下 image.png

image.png

竟然报超时了,是不是非常诡异,那么为什么会有这种情况出现呢?这代码看起来并没什么区别,在说原因之前,需要先短暂回顾一下事务的执行原理,首先我们会解析带有事务注解的方法产生代理对象,并且在最终创建事务链接的方法之前会把超时时间加上系统时间设置到connetcion中,而执行sql的时候,mybatis会获取到connetcion然后判断这个时间是否已经超时,没有超时则执行数据库操作,代码如下,这就像一个击鼓传花的游戏,同一个事务内最后一个执行sql的mapper如果拿到链接的时间没有超时,则不会抛异常,那么为什么移到InsertTest()里面却不会超时呢,因为这是一个新的事务,时间会重新开始计算,所以这是一个事务里面最后一个sql执行的时间是否超时而不是整个事务方法的执行时间超时,也就是说,一个事务方法内,当你sql执行完了,后面再做其他事务的事情或者说一些业务逻辑是不会超时的,所以用这个参数的时候需要特别小心

image.png


rollback-for&no-rollback-for

rollback-for属性主要用来指定异常来回滚,默认是runtimeException,没有设置的话抛非runtimeException是不会进行回滚的,也可以用no-rollback-for来指定不需要回滚的异常


Spring事务实用技巧

优雅的回滚

1.如果事务方法中执行抛异常,我们会怎么处理?如果想回滚的话,那就不处理让异常抛出,此时返回是异常错误,如果想正常返回的话那就需要捕获异常,设置错误码,但是异常被捕获sql不会回滚,那么有没有一种方式,可以让我正常返回,但是又能够回滚的呢?当然可以,使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()可以回滚当前事务,代码如下,只需要捕获异常,设置setRollbackOnly,就可以做到正常返回并且此部分事务会回滚

image.png

事务代码块

2.有时候并不想事务方法内所有方法都使用事务,只需要关键点才需要事务,一般实现这种方法可能都是将需要事务的逻辑封装为一个方法,下面提供一种更灵活的代码块的事务控制方法 TransactionTemplate image.png

我们可以通过编程式方法更好的管理事务以及将事务颗粒度减少,可以提高性能,避免大事务,但也有一定的局限性,使用这个方法后就没办法使用事务的传播特性了,只能同一个事务内使用,并且需要注意的是, doInTransaction方法内一定要捕获异常,如果有异常抛出则会导致事务回滚,原因如下

image.png

上图可以看出,如果doInTransaction没有捕获异常,上层会捕获并且将代码回滚,如果这个啊啊方法

方法上层还有事务传播到这层,那将会导致整个事务回滚,如insertUser调用insertAll,如果doInTransaction抛错触发rollbackException,在insertUser捕获insertAll也是没用的,最后还是会回滚。

事务状态监听

3.我们还可以在事务执行的各个阶段进行监听和回调,如下图,使用这个方法我们可以在 事务挂起和恢复,以及提交前后写入我们的业务逻辑

image.png


Spring事务属性&实用技巧小结

本篇主要讲解Spring事务的属性以及里面的一些坑还介绍了一些实用技巧,如果觉得对你 有帮助的话,别忘了点赞喔,希望对你有所帮助我是老道,只做最肝的干货分享蟹蟹

下篇预告

Spring事务篇暂时告一段落,还有失效场景有空再补上,其实你明白了原理后你也大致能推敲出Spring事务失效的原因了,比如没有开启代理注解,Spring没扫描到包,同一个类中创建新事务失效等等,接下来我会开始并发编程系列,会讲到常用的list,map底层,还有线程池,锁,JMM共享模型,还有中间件ELK实战系列,各位敬请期待吧,先关注不迷路,蟹蟹大家