Spring声明式事务注意事项
[toc]
事务失效
事务失效我们一般要从两个方面排查问题
数据库层面
数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的
1 | alter table table_name engine=myisam; |
这样就会出现“事务失效”的问题了
解决方案:修改存储引擎为Innodb。
业务代码层面
业务层面的代码是否有问题,这就有很多种可能了
- 我们要使用Spring的声明式事务,那么需要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有@Service、Component等一系列注解
解决方案:将Bean交由Spring进行管理(添加@Service注解)
- @Transactional注解是否被放在了合适的位置。默认情况下你无法使用@Transactional对一个非public的方法进行事务管理
解决方案:修改需要事务管理的方法为public。
- 出现了自调用。什么是自调用呢?我们看个例子
1 | @Service |
上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveA跟saveB方法,这就是自调用。在上面的例子中saveA跟saveB上的事务会失效
那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类
1 | public class DmzServiceProxy { |
上面是一段伪代码,通过startTransaction、rollbackTransaction、commitTransaction这三个方法模拟代理类实现的逻辑。因为目标类DmzService中的saveA跟saveB方法上存在@Transactional注解,所以会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB方法上没有@Transactional,相当于代理类直接调用了目标类中的方法。
我们会发现当通过代理类调用saveAB时整个方法的调用链如下:
实际上我们在调用saveA跟saveB时调用的是目标类中的方法,这种情况下,事务当然会失效。
解决方案:
- 自己注入自己,然后显示的调用,例如:

这种方案看起来不是很优雅
- 利用AopContext,如下:

使用上面这种解决方案需要注意的是,需要在配置类上新增一个配置
1 | // exposeProxy=true代表将代理类放入到线程上下文中,默认是false |
总结

事务回滚相关问题
回滚相关的问题可以被总结为两句话
- 想回滚的时候事务却提交了
- 想提交的时候被标记成只能回滚了(rollback only)
先看第一种情况:想回滚的时候事务却提交了。这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。
Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务,已经执行的SQL会提交掉。
如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
对应代码其实我们上篇文章也分析过了,如下:

以上代码位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中
默认情况下,只有出现RuntimeException或者Error才会回滚
1 | public boolean rollbackOn(Throwable ex) { |
所以,如果你现在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:
1 | @Transactional(rollbackFor = Exception.class) |
第二种情况:想提交的时候被标记成只能回滚了(rollback only)。
对应的异常信息如下:
1 | Transaction rolled back because it has been marked as rollback-only |
先来看个例子吧
1 | @Service |
在上面这个例子中,DmzService的testRollbackOnly方法跟IndexService的a方法都开启了事务,并且事务的传播级别为required,所以当我们在testRollbackOnly中调用IndexService的a方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexService的a方法抛出了异常,但是我们在testRollbackOnly将异常捕获了,那么这个事务应该是可以正常提交的,为什么会抛出异常呢?
源码分在处理回滚时有这么一段代码

在提交时又做了下面这个判断(这个方法我删掉了一些不重要的代码)

可以看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中,并且unexpected传入的为true。在处理回滚时又有下面这段代码

最后在这里抛出了这个异常。
以上代码均位于AbstractPlatformTransactionManager中
总结起来,主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记,所以即使我们在外部事务中catch了抛出的异常,整个事务仍然无法正常提交,并且如果你希望正常提交,Spring还会抛出一个异常。
解决方案:
这个解决方案要依赖业务而定,你要明确你想要的结果是什么
- 内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务

虽然这两者都能得到上面的结果,但是它们之间还是有不同的。
当传播级别为requires_new时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。
但是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。
- 内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,但是方法不抛出异常

通过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码

最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是预期之中的,所以在处理完回滚后并不会抛出异常。
1、@Transactional 应用在非 public 修饰的方法上
如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。

之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
1 | protected TransactionAttribute computeTransactionAttribute(Method method, |
此方法会检查目标方法的修饰符是否为 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属性。

1 | // 希望自定义的异常可以进行回滚 |
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring源码如下:
1 | private int getDepth(Class<?> exceptionClass, int depth) { |
4、同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
1 | //@Transactional |
5、异常被你的 catch“吃了”导致@Transactional失效
这种情况是最常见的一种@Transactional注解失效场景,
1 | @Transactional |
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能!
会抛出异常:
1 | 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,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。
6、数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。
转载自: