参考:
https://www.cnblogs.com/xhq1024/p/13085280.html
https://www.cnblogs.com/ynyhl/p/12066530.html
https://blog.csdn.net/weixin_40920882/article/details/105740380
@Transaction注解失效的几种场景
一、@Transactional介绍
1、@Transactional注解可以作用于哪些地方?
@Transactional 可以作用在接口
、类
、类方法
上
。
- 作用于类:表示所有该类的
public
方法都配置相同的事务属性信息。 - 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
- 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。
2、@Transactional注有哪些属性?
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.DEFAULT
。
-
- Isolation.DEFAULT:默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是:READ_COMMITTED,Mysql为REPEATABLE_READ。
-
Isolation.READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
-
Isolation.READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
-
Isolation.REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
-
Isolation.SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
二、@Transactional失效场景
1.@Transactional 应用在非 public 修饰的方法上
如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。
之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
2、@Transactional 注解属性 propagation 设置错误
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
3、@Transactional 注解属性 rollbackFor 设置错误
// 希望自定义的异常可以进行回滚 @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
private int getDepth(Class<?> exceptionClass, int depth) { if (exceptionClass.getName().contains(this.exceptionName)) { // Found it! return depth; } // If we've gone as far as we can go and haven't found it... if (exceptionClass == Throwable.class) { return -1; } return getDepth(exceptionClass.getSuperclass(), depth + 1); }
4、同一个类中方法调用,导致@Transactional失效
public void A() throws Exception { /** * 调用B方法 */ this.B(); ... ... } @Transactional() public void B() throws Exception { mapper.insert(); }
5、异常被你的 catch“吃了”导致@Transactional失效
这种情况是最常见的一种@Transactional注解失效场景因为当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
,那事务就从根本上失效了。
分析spring事务@Transactional注解在同一个类中的方法之间调用不生效的原因及解决方案
问题:
在Spring管理的项目中,方法A使用了Transactional注解,试图实现事务性。但当同一个class中的方法B调用方法A时,会发现方法A中的异常不再导致回滚,也即事务失效了。
当这个方法被同一个类调用的时候,spring无法将这个方法加到事务管理中。
我们来看一下生效时候和不生效时候调用堆栈日志的对比。
通过对比两个调用堆栈可以看出,spring的@Transactional事务生效的一个前提是进行方法调用前经过拦截器TransactionInterceptor,也就是说只有通过TransactionInterceptor拦截器的方法才会被加入到spring事务管理中,查看spring源码可以看到,在AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice方法中会从调用方法中获取@Transactional注解,如果有该注解,则启用事务,否则不启用。
这个方法是通过spring的AOP类CglibAopProxy的内部类DynamicAdvisedInterceptor调用的,而DynamicAdvisedInterceptor继承了MethodInterceptor,用于拦截方法调用,并从中获取调用链。
如果是在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用,必须将方法放入另一个类,并且该类通过spring注入。
原因:
Transactional是Spring提供的事务管理注解。
重点在于,Spring采用动态代理(AOP)实现对bean的管理和切片,它为我们的每个class生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。
而在同一个class中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象。所以Spring无法切到这次调用,也就无法通过注解保证事务性了。
也就是说,在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用。
解决方法1:
将事务方法放到另一个类中(或者单独开启一层,取名“事务层”)进行调用,即符合了在对象之间调用的条件。
解决方法2:
获取本对象的代理对象,再进行调用。具体操作如:
1) Spring-content.xml上下文中,增加配置:<aop:aspectj-autoproxy expose-proxy="true"/>
2) 在xxxServiceImpl中,用(xxxService)(AopContext.currentProxy()),获取到xxxService的代理类,再调用事务方法,强行经过代理类,激活事务切面。
解决方法3:
很多时候,方法内调用又希望激活事务,是由于同一个方法既有DAO操作又有I/O等耗时操作,不想让耗时的I/O造成事务的太长耗时(比如新增商品同时需要写入库存)。此时,可以将I/O做成异步操作(如加入线程池),而加入线程池的操作即便加入事务也不会导致事务太长,问题可以迎刃而解。
解决方法4:
用@Autowired 注入自己 然后在用注入的bean调用自己的方法也可以
参考:
https://blog.csdn.net/ligeforrent/article/details/79996797
https://www.jianshu.com/p/2e4e1007edf2
@Transactional失效的几种情况
可以作用的地方
-
类:表示该类的所有public方法都会配置相同的事务属性信息
-
方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务属性信息
-
接口:不推荐使用,一旦注解在interface上并且配置了Spring AOP使用CGLib动态代理,将会导致@Transactional失效
属性
propagation
代表事务的传播级别,默认Propagation.REQUIRED
- Propagation.REQUIRED:
如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务 - Propagation.SUPPORTS
如果当前存在事务,则加入事务,没有则一飞事务方式运行 - Propagation.MANDATORY
当前存在事务,则加入事务,不存在事务则抛出异常 - Propagation.REQUIRED_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
嵌套事务
isolation
事务隔离级别,默认Isolation.DEFAULT(底层数据库默认隔离级别)
- Isolation.READ_UNCOMMITTED
- Isolation.READ_COMMITTED
- Isolation.REPEATABLE_READ
- Isolation.SERIALIZABLE
timeout
事务超时时间,默认-1.如果超过该时间事务依然没有完成,回滚
readonly
指定事务为只读事务,默认false,为了忽略那些不需要事务的方法,比如读取数据,可以设置read-only为true
rollbackFor
触发事务回滚的异常,可以指定多个
norollbackFor
抛出指定的异常,不回滚事务,可以指定多个
失效的场景
- @Transaction应用在非public修饰的方法上
AOP代理的时候,在上图TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CGLIBAOPProxy的内部类)的intercept方法或者JdkDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法获取Transaction注解的事务配置信息
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法会检查目标方法的修饰符是否为public ,不是public的不好获取Transaction注解,虽然事务无效,但不会有任何报错
- 注解属性propagation设置错误
以下三种情况,事务将不会进行回滚
TransactionDefinition.PROPAGATION_SUPPORTS:
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
TransactionDefinition.PROPAGATION_NEVER
- 注解属性rollbackFor设置错误
rollbackFor可以指定能够触发事务回滚的异常类型,Spring默认抛出未检出unchecked的异常(继承RunTimeException的异常)或者Error才会回滚事务,其他异常不会触发事务,如果事务中抛出其他异常类型,需要指定rollbackFor属性
// 希望自定义的异常可以进行回滚
@ransactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
如果抛出的异常事务rollbackFor指定异常的子类,事务同样会进行回滚
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
同一个类中的方法调动
-
开发中避免不了同一个类中的方法调用,类test中的A方法调用B方法,A没有声明注解事务,B方法有,外部调用方法A之后,B的事务是不会起效果的,原因是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理
-
异常被catch,导致@Transaction失效
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
当B方法内部抛出异常,此时A方法try catchB方法的异常,那么这是事务能正常回滚吗?
答案是不能
会抛出异常
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
当ServiceB中抛出异常,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动捕获了一个异常并处理,ServiceA会认为当前事务应该正常commit,这个时候就会出现前后不一致,这样就会出现上面的异常
Spring的事务在调用业务方法之前开始的,业务方法执行完毕之后才会执行commit 或者rollback,事务是否执行取决于是否爬出runtime异常,如果抛出runtime 异常,并在你的业务代码中并有catch到,事务就会回滚
在业务方法中一般不需要catch异常,非要catch就一定要throw new RunTimeExecption() 或者抛出注解中指定会回滚的异常,否则将会导致事务失效,数据commit造成数据不一致
- 数据库存储引擎不支持
比如MyISAM
注意:
spring的事务边界是在调用业务方法之前开始的,业务方法执行完毕之后来执行commit or rollback(spring默认取决于是否抛出runtime异常).
如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
一般不需要在业务方法中catch异常,如果非要catch,在做完你想做的工作后(比如关闭文件等)一定要抛出runtime exception,否则spring会将你的操作commit,这样就会产生脏数据.所以你的catch代码是画蛇添足。