提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
1. @Transactional是什么?
@Transactional
是开启声明式事务的方法。声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
简而言之,
@Transactional
注解在代码执行出错的时候能够进行事务的回滚
2. @Transactional怎么用?(SpringBoot)
- 在启动类上添加
@EnableTransactionManagement
注解。
springboot 1.x
使用事务需要在引导类上添加@EnableTransactionManagement
注解开启事务支持
springboot 2.x
可直接使用@Transactional
玩事务,传播行为默认是REQUIRED
- 用于类上时,该类的所有
public
方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。 - 在项目中,
@Transactional(rollbackFor=Exception.class)
,如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。 - 在
@Transactional
注解中如果不配置rollbackFor
属性,那么事物只会在遇到RuntimeException
的时候才会回滚,加上rollbackFor=Exception.class,
可以让事物在遇到非运行时异常时也回滚。
3. @Transactional的参数?
3.1 事务7种传播行为
引用文章:https://www.jb51.net/article/224832.htm#_label3_0_3_0
多事务方法之间相互调用,这个过程中事务是如何管理的问题(例如两个事务调用对方方法,事务该如何选择)
传播行为指的是,如果多个事务进行嵌套运行,子事务是否要和大事务共用一个事务
里面有多个选择:
-
REQUIRED
支持当前事务,如果不存在,就新建一个 -
SUPPORTS
支持当前事务,如果不存在,就不使用事务 -
MANDATORY
支持当前事务,如果不存在,抛出异常 -
REQUIRES_NEW
如果有事务存在,挂起当前事务,创建一个新的事务 -
NOT_SUPPORTED
以非事务方式运行,如果有事务存在,挂起当前事务 -
NEVER
以非事务方式运行,如果有事务存在,抛出异常 -
NESTED
如果当前事务存在,则嵌套事务执行(嵌套式事务)
也有人这么解释,可以结合理解:
REQUIRED
:如果有事务运行,当前方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行;
NOT_SUPPORTED
:当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。
REQUIRES_NEW
:当前方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起。
MANDATORY
:当前的方法必须运行在事务内部,如何没有正在运行的事务,就抛出异常。
SUPPORTS
:如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中。
NEVER
:前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。
NESTED
:如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。
这七种事务传播机制最常用的就两种:
REQUIRED
:一个事务,要么成功,要么失败
REQUIRES_NEW
:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务(指REQUIRES_NEW
不受REQUIRED
影响,反过来不行)
伪代码练习:
传播行为伪代码模拟:有a,b,c,d,e等5个方法,a中调用b,c,d,e方法的传播行为在小括号中标出
a(required){
b(required);
c(requires_new);
d(required);
e(requires_new);
// a方法的业务
}
问题:
- a方法的业务出现异常,会怎样?a,b,d回滚 c,e不回滚
- d方法出现异常,会怎样?a,b,d回滚;c不回滚;e未执行
- e方法出现异常,会怎样?a,b,d,e回滚 c不回滚,e方法出异常会上抛影响到上级方法
- b方法出现异常,会怎样?a,b回滚 c,d,e未执行
加点难度:
a(required){
b(required){
f(requires_new);
g(required)
}
c(requires_new){
h(requires_new)
i(required)
}
d(required);
e(requires_new);
// a方法的业务
}
问题:
- a方法业务出异常?a,b,g,d回滚;f,c,h,i,e不回滚
- e方法出异常?e,a,b,g,d回滚;f,c,h,i不回滚
- d方法出异常?a,b,g,d回滚;f,c,h,i不回滚;e未执行
- h,i方法分别出异常?h,i,c,a,b,g回滚;f不回滚;d,e未执行
- i方法出异常?i,c,a,b,g回滚;f,h不回滚;d,e未执行
- f,g方法分别出异常?f,g,b,a回滚;c,h,i,d,e未执行
前面这些都是在不catch异常的前提下
任何处崩,已经执行的requires_new不会崩
a(required){
b(required);//如过b设置了超时,超时不会生效的,因为a的required会传播给b
}
required:将之前事务用的connection传递给这个方法使用
required_new:这个方法直接使用新的connection
3.3.1关于嵌套事务的注意事项
案例:
有下面一接口SpuInfoService
public interface SpuInfoService extends IService<SpuInfoEntity> {
PageVo queryPage(QueryCondition params);
PageVo querySpuInfo(QueryCondition condition, Long catId);
void saveSpuInfoVO(SpuInfoVO spuInfoVO);
void saveSku(SpuVo spuVo, Long spuId);
void saveBaseAttr(SpuVo spuVo, Long spuId);
void saveSpuDesc(SpuVo spuVo, Long spuId);
Long saveSpu(SpuVo spuVo);
}
它的实现类是SpuInfoServiceImpl
测试1:同一service + requires_new
添加事务:
这时,在保存商品的主方法中制造异常
bigSave
的事务是REQUIRED
、 saveSpu
是REQUIRED
、 saveSpuDesc
是REQUIRES_NEW
bigSave
的事务嵌套saveSpu
和saveSpuDesc
也就是
bigSave(required){
saveSpu(requires_new);
saveSpuDesc(required)
}
预想的是如果bigSave
的事务操作种出现异常,saveSpu
会回滚,saveSpuDesc
不会回滚
实际确实都会回滚,其实saveSpu在本例中加不加事务都一样,因为saveSpu
加的是REQUIRED
REQUIRED
如果有事务运行,当前方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行,因为外层是事务的,那么内层就如果出现异常一会回滚
关键在于 saveSpuDesc
是REQUIRES_NEW
,我们希望出现异常不让它回滚,因为Spring事务实际是基于AOP的,而AOP的底层是动态代理,也就是说Spring事务也是动态代理:通过代理对象开启事务,提交事务、回滚事务。
我们上图的调用方式本质是通过this调用的不是通过代理对象,这也就是为什么saveSpuDesc
事务不生效的原因,所以事务要生效必须是代理对象在调用
解决方式1:把saveSpuDesc
放在另外一个SpuDescService
中
把saveSpuDesc方法放到SpuDescService中
改造SpuServiceImpl中保存商品的方法,调用SpuDescServiceImpl的saveSpuDesc方法:
spuDescService:
通过其他service对象(spuDescService)调用,这个service对象本质是动态代理对象
有事务的业务逻辑,容器中保存的是这个业务逻辑的代理对象
this:
解决方式2:创建本Service的代理对象
只需要把测试1中的this.方法名()替换成this代理对象.方法名()即可。
问题是怎么在service中获取当前类的代理对象?
在类中获取代理对象分三个步骤:
- 导入aop的场景依赖:spring-boot-starter-aop
- 开启AspectJ的自动代理,同时要暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)
- 获取代理对象:SpuInfoService proxy = (SpuInfoService) AopContext.currentProxy();
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 回滚策略
事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。
默认情况下的回滚策略:
- 运行时异常:不受检异常,没有强制要求try-catch,都会回滚。例如:ArrayOutOfIndex,OutofMemory,NullPointException
- 编译时异常:受检异常,必须处理,要么try-catch要么throws,都不回滚。例如:FileNotFoundException
可以通过@Transactional注解的下面几个属性改变回滚策略:
rollbackFor
:指定的异常必须回滚noRollbackFor
:发生指定的异常不用回滚
3.2.1 测试编译时异常不回滚
在商品保存方法中制造一个编译时异常:
重启测试,注意pms_spu表中数据:
控制台报异常:
pms_spu表中的数据新增成功了
也就证明了编译时异常不回滚。
3.2.2 定制回滚策略
经过刚才的测试,我们知道:
ArithmeticException异常(int i = 1/0)会回滚FileNotFoundException异常(new FileInputStream(“xxxx”))不回滚
接下来我们来改变一下这个策略:
测试:
FileNotFoundException:在程序中添加newFileInputStream(“xxxx”),然后测试。
3.3 超时事务
@Transactional注解,还有一个属性是timeout
超时时间,单位是秒。
timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。
即:超时时间(timeout)是指数据库超时,不是业务超时。
改造之前商品保存方法:SpuInfoServiceImpl类中
重启测试:控制台出现事务超时异常
3.4 只读事务
@Transactional注解一个属性是只读事务属性
如果一个方法标记为readOnly=true
事务,则代表该方法只能查询,不能增删改。readOnly默认为false
给商品新增的事务标记为只读事务:
测试:
3.5 隔离级别
使用Isolation
参数可以设置事务的隔离级别
事务并发引起一些读的问题:
- 脏读:一个事务可以读取另一个事务未提交的数据
- 不可重复读: 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
- 虚读(幻读: 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点
并发写:使用mysql默认的锁机制(独占锁)
解决读问题:设置事务隔离级别
- read uncommitted(0)
- read committed(2)
- repeatable read(4)
- Serializable(8)
最后,有必要补充一下常用数据库的默认隔离级别:
MYSQL:默认为REPEATABLE_READ
SQLSERVER:默认为READ_COMMITTED
Oracle:默认隔离级别 READ_COMMITTED
注意:mysql数据库,当且仅当引擎是InnoDB,才支持事务(MyIsam引擎不支持事务)。
4. @Transactional 失效场景
引用文章:https://jiming.blog.csdn.net/article/details/110181822
1. @Transactional 应用在非 public 修饰的方法上
之所以会失效,是因为在 Spring AOP 代理时(如上图所示)TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息,方法如下:
此处我直接去了最关键得代码以展示,如图所见,此方法会检查目标方法的修饰符为 public
,不是 public 则不会获取 @Transactional 的属性配置信息
。这就是原因所在!!
注意:protected、private 修饰的方法上使用 @Transactional 注解
,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
2. @Transactional 注解属性 propagation 设置错误
这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行;
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起;
- TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3. @Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。
Spring默认抛出了未检查(unchecked)异常(继承自 RuntimeException 的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。
如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
注意:若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚,Spring源码如下:
4. 在同一个类中方法调用,导致 @Transactional 失效
这也是经常犯错误的一个地方,要特别注意!!
开发中避免不了会对同一个类里面的方法调用,比如:有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。
那为啥会出现这种情况?
其实,这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码直接调用时,才会由Spring生成的代理对象来管理,进而由 TransactionInterceptor (事务拦截器)生成事务对象。
5. 异常被 catch 处理了,导致 @Transactional 失效
这种情况也是最常见的一种 @Transactional 注解失效场景,甚至很多人都不能准确定位到这个失效点。
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案是:一定不能。而且会抛出异常:“org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only”
原因如下:
-
当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
-
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出 RuntimeException。如果抛出 RuntimeException 并在你的业务方法中没有catch到的话,事务会回滚。
解决办法:
在业务方法中一般不需要catch异常,如果非要catch一定要抛出 throw new RuntimeException()
,或者注解中指定抛异常类型 @Transactional(rollbackFor=Exception.class)
,否则会导致事务失效,数据 commit 造成不一致。
所以,try…catch…也要活学活用,使用不当反倒会画蛇添足。
6. 数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。
常用的MySQL数据库默认使用支持事务的Innodb引擎。一旦数据库引擎切换成不支持事务的MyIsam,那事务就从根本上失效了。
5.总结
@Transactional 注解的看似简单易用,但如果对它的用法一知半解,还是会踩到很多坑的。
- @Transactional 应用在非 public 修饰的方法上,不支持回滚;
- @Transactional 注解属性 propagation 设置错误;
- @Transactional 注解属性 rollbackFor 设置错误;
- 在同一个类中方法调用,导致 @Transactional 失效;
- 异常被 catch 处理了,导致 @Transactional 没办法回滚而失效;
- 数据库引擎不支持事务