@Transactional注解失效的6种场景分析以及原理
@Transactional注解相信大家并不陌生,平时开发中常用的一个注解,它能保证方法内多个数据库操作要么同时成功,要么同时失败回滚。但是使用@Transactional有许多需要注意的细节,不然你会发现你的@Transactional总是莫名其妙的失效了。
事务
事务管理在系统开发中是不可缺少的一部分,Spring提供了很好的事务管理机制,主要分为编程事务和声明事务。
编程事务:
是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强,如下所示:
声明事务:
基于AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明事务用的比较多。声明事务也有两种实现方式,一是基于TX和AOP的XML配置文件的方式,二就是基于@Transactional注解的声明式事务了。
@Transactional的介绍
@Transactional可以用在接口、类、类方法上。
-
作用类:当把@Transactional注解放在类上时候,表示该类中所有的public方法都配置相同的事务属性信息。
-
作用方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
-
作用接口:不推荐这种使用方式,因为一旦标注在接口上并且配置了Spring AOP使用CGLib动态代理,将会导致@Transactional事务失效。
@Transactional注解有哪些属性
propagation属性
propagation代表事务的传播行为,默认值为Propagation.REQUIRED。
其他属性如下:
-
Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
-
Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
-
Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
-
Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
-
Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
-
Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
-
Propagation.NESTED :和 Propagation.REQUIRED 效果一样。
isolation属性
isolation:事务的隔离级别,默认值为:Isolation.DEFALUT。
-
TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
-
TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
-
TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
-
TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
-
TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
其实,仔细看发现这就是数据库的隔离级别。
timeout属性
timeout:事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
rollbackFor属性
用于指定能够触发回滚的异常类型,可以指定多个类型。
readonly属性
指定事务是否为只读事务,默认值为false;为了忽略那些不需要的事务的方法,比如读取数据,可以设置read-only为true。
noRollbackFor属性
抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
@Transactional失效场景
这是重点!!!
1、@Transactional应用在非public方法上
前面说了,该注解只能应用于public方法。想想也很容易理解,只要public方法回滚了,那么public方法内部的调用的方法也就都回滚了,只要一个回滚的入口就好了。
原因:
之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
return null;
}
此方法会检查目标方法的修饰符是否为public,不是的话,则不会获取@Transactional注解的配置信息,所以会失效。
注意:
protected、private修饰的方法上使用@Transactional注解,虽然事务无效,但不会有任何的报错信息。
2、@Transactional注解属性propagation设置错误
这种失效是由于配置错导致的,若是错误的配置一下三种,事务将不会发生回滚。
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3、@Transactional注解属性rollbackFor错误
这个更好理解了,rollbackFor属性配置当发生何种异常的时候会回滚。一般我们都是配置的Exception异常大类。但是在源码中我们可以知道:
/**
* Defines zero (0) or more exception {@link Class classes}, which must be
* subclasses of {@link Throwable}, indicating which exception types must cause
* a transaction rollback.
* <p>By default, a transaction will be rolling back on {@link RuntimeException}
* and {@link Error} but not on checked exceptions (business exceptions). See
Spring默认抛出未检查异常,继承自RuntimeException或者Error时才回滚事务;其他异常不会回滚。所以当我们希望发生我们指定的异常时回滚,那么就需要配置这个属性了。
4、@Transactional在同一个方法中调用,导致@Transactional失效 (重点关注)
开发中避免不了类中方法的调用,比如有一个A类,内有B、C两个public方法。B声明事务,C未声明事务。当B调用C,那么B失败则事务回滚;但是当C调用B的时候,C失败,那么事务不会回滚。
原因:
其实还是因为Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用的时候,才会由Spring生成的代理对象来管理。
5、异常被吃了,导致回滚失败
当你对方法使用@Transactional的时候,但是你方法内部try-catch了异常。
举例:
A、B两个方法,A声明事务;A方法调用B方法并且try-catch了B方法,当B方法执行报错的时候,A还能回滚吗?
答案:不能!此时会抛出如下异常 —
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,那么事务就从根本上失效了。
总结:
我们日常开发中,建议将事务注解标注在controller类上,作为类注解使用,对于部分查询类的接口设置其属性为read-only即可。这样即使得代码简洁明了,其次又不会导致因为乱使用事务直接导致的事务不生效的问题。
申引:
关于CGLib动态代理,包括@Sync异步注解在内,需要注意同类中调用异步不生效的问题,原因还是因为外部类访问时才会通过代理使得注解生效;内部调用则不会通过代理,所以内部调用相关方法时会使得@Sync失效。