在使用Spring注解进行事务控制的时候,我们都习惯性用@Transactional 注解进行处理,但是指值得注意的一点是真正出现异常的时候,你的事务真正回滚了吗?以下就列举几种添加@Transactional但是事务回滚没有生效的情景。
一、除非特殊配置只有定义在 public 方法上的 @Transactional 才能生效
原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。
二、必须通过代理过的类从外部调用目标方法才能生效
Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。
(1)同一个类中通过this调用的场景
图1中的代码逻辑通过this调用,抛出RuntimeException 是不会回滚事务的,具体原因是this 指针代表对象自己,而不是Spring注入的代理。而只有通过代理访问的方法才有可能进行回滚。而代理和this 的区别如图2所示:this指的是TestController这个类,而真正经历Spring注入的代理是有“CGLIB”字样的testService。只有经过testService代理直接调用的方法在出现异常的时候才会进行回滚。
三、不是所有的异常都会触发回滚
默认情况下,出现 RuntimeException(未检查异常)或 Error 的时候,Spring 才会回滚事务。此处需要解释一下Java中的异常:
Java中异常分为两大类:checkedexception(检查异常)和unchecked exception(未检查异常)。
未检查异常也可以叫做RuntimeException(运行时异常),他们的主要区别:对于运行时异常,java编译器不要求捕获或者一定要继续抛出,但是必须捕获或者抛出检查异常。
常见的检查异常:Exception,FileNotFoundException,IOException,SQLException。
常见的未检查异常: NullPointerException,ClassCastException,ArrayIndexsOutOfBoundsException,ArithmeticException(算术异常,除0溢出)。
(1)如果在代码中对异常进行了try catch 操作使得RuntimeException(未检查异常)或 Error 没有抛出来,此时事务业务不会回滚,也可以正常执行。如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置让当前事务处于回滚状态。具体代码如图3所示。
(2)在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制)可以使用如下代码对所有的异常进行回滚。
@Transactional(rollbackFor = Exception.class)
四、多次数据库操作回滚互不影响
在一个事务中有多个更新数据库的操作,但是又不想因为某一个数据库操作的回滚影响到其他操作的完成。
为了描述方便我们把不需要回滚的称为主流程,能够回滚的是子流程。如果不做额外处理的话子流程如果回滚,即使子流程的代码被try catch 住由于子流程已经将该事务标注为回滚事务,最后主流程也会跟着回滚。
解决这个问题的办法让子流程在独立事务中运行,也就是为子流程注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务即可。
@Transactional(propagation = Propagation.REQUIRES_NEW)
总结:
(1)我们务必确认调用 @Transactional 注解标记的方法是 public 的,并且是通过 Spring 注入的 Bean 进行调用的。
(2)因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。
(3)如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。