一、前言
emm,又又又踩坑啦。这次的需求主要是对逾期计算的需求任务进行优化,现有的计算任务运行时间太长了。简单描述下此次的问题:在项目中进行多个数据库执行操作时,我们期望的是将其整个封装成一个事务,要么全部成功,或者全部失败,然而在自测异常场景时发现,里面涉及的第一个数据状态更新成功了,但是后面的数据在插入出现异常,后面查询数据表发现,该数据的状态已经被更新成功啦。
emmm,查看代码发现确实是使用了@Transactional注解没问啊。于是通过查询网上相关资料发现,在使用Spring中事务注解@Transactional时会存在几种场景下该注解失效,即不能按照预期封装成一个事务操作,于是对该注解进行学习并对相关失效场景进行分析,整理文章如下;
二、@Transactional注解失效场景实例验证
1、@Transactional注解属性
2、 propagation属性
propagation代表事务的传播行为,默认值为Propagation.REQUIRED
3、 @Transactional注解使用场景?
@Transactional注解可以作用在接口、类、类方法中。
当作用于类时,表示所有该类的public方法都配置相同的事务属性信息。
当作用于方法时,当类配置了@Transactional注解,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
当作用于接口时,不推荐使用,因为在接口使用@Transactional并且配置了Spring AOP使用CGLib动态代理将会导致其失效。
4、 @Transactional注解失效场景?
- @Transactional注解作用在非public修饰的方法上,会失效。
失效原因:在Spring AOP代理时,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy的内部类)的Intercept方法或JDKDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransationAttributeSource的computeTransactionAttribute方法,获取@Transactional注解的事务配置信息。
1 protected TransactionAttribute computeTransactionAttribute(Method method,
2 Class<?> targetClass) {
3 // Don't allow no-public methods as required.
4 if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
5 return null;
6 }
此方法会检查目标方法的修饰符是否为public,非public作用域则不会获取@transactional的属性配置信息。其中protected、private修饰的方法上使用 @Transactional注解,事务会失效但不会有任何报错。
- @Transactional注解属性propagation设置错误导致注解失效
失效原因:配置错误, PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER三种事务传播方式不会发生回滚。
▪ 实例验证:写了一个demo进行测试。demo主要功能如下:执行两次数据库插入操作,并在扩展信息字段中添加备注;
▪ 运行结果如下,构造的单号不存在订单查询为空触发异常,观察数据库发现,第一次数据库插入操作已经执行成功,故而验证@Transactional注解失效;
- @Transactional注解属性rollbackFor设置错误导致注解失效
rollbackFor可以指定能够触发事务回滚的异常类型。Spring默认抛出了unchecked异常(继承自RuntimeException)或者Error才会回滚事务。若事务中抛出了其他类型的异常,但却期望Spring能够回滚事务,就需要指定rollbackFor属性,否则就会失效。
- 同一类中方法调用,导致@Transactional失效
比如类demo中有方法A和B,方法B中使用@Transactional注解,方法A没有注解,但是demo类通过方法A调用方法B,像这种间接调用会导致方法B中的@Transactional事务注解失效。
失效原因:只有当事务方法被当前类以外的代码调用时,才会有Spring生成的代理对象管理。(Spring AOP代理机制造成的)。
▪ 实例验证:demo中构造场景为在同一个类中,在test方法中添加@Transactional注解,querRiskScore方法中不添加该注解,然后在querRiskScore方法中调用test方法;观察下多个插入操作是否会因为异常而中断回滚;
▪ 运行结果如下,还是通过构造的单号不存在订单查询为空触发异常,观察数据库发现,第一次数据库插入操作已经执行成功,第二次数据插入操作失败,并没有因为异常而触发事务操作,故而验证@Transactional注解方法间的调用会失效;
- 多线程任务可能导致@Transaction案例失效
失效原因:线程不属于Spring托管,故线程不能够默认使用Spring的事务,也不能获取Spring注入的bean,在被Spring声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制。
- 异常被方法内catch捕获导致@Transactional失效
比如B方法内部抛了异常,而A方法此时try-catch了B方法的异常,则该事务不能正常回滚。
失效原因:因为B方法中抛出异常以后,标识当前事务需要rollback,但是A方法中由于你手动的捕获这个异常并进行处理,A方法认为当前事务应该正常commit,此时就出现前后不一致,会抛出org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常。
▪ 实例验证:这个场景的本质还是异常被捕获导致无法正常的抛出,进而导致@Transactional注解无法正常工作,我简化了下demo实例场景,构造场景如下:在querRiskScore方法中添加@Transactional注解,然后在querRiskScore方法中对异常进行捕获;观察下多个插入操作是否会因为异常而中断回滚;
▪ 运行结果如下,还是通过构造的单号不存在订单查询为空触发异常,但是我们在方法内部对该异常进行捕获,并未向上层抛出,我们期望的场景是两次数据插入执行失败,但是观察数据库发现,第一次数据库插入操作已经执行成功,第二次数据插入执行成功,与我们的预期结果不符,故而验证@Transactional注解在方法中异常被捕获的场景中会失效;
究其原因:Spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit 或 rollback,事务是否执行取决于是否抛出runtime异常,如果抛出runtime exception并在你的业务方法中并没有catch到的话,事务就会回滚。
三、“事务”知识回顾
1.什么是事务?
通常我们所指的事务是指数据库事务,使用数据库事务有以下两处优点:
当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
事务为数据库操作序列提供了一个从失败恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。
2. 事务具有的特性?
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性,简称事务的ACID特性。
- 原子性
- 一致性
- 隔离性
- 持久性
3. 什么是Spring中的事务?
Spring中同样提供了很好的事务管理机制,主要分为编程式事务和声明式事务。
- 编程式事务
是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。编程式事务方式需要开发者在代码中手动的管理事务的开启、提交、回滚等操作。
public void test() {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 事务操作
// 事务提交
transactionManager.commit(status);
} catch (DataAccessException e) {
// 事务提交
transactionManager.rollback(status);
throw e;
}
}
- 声明式事务
声明式事务是基于AOP面向切面,它将具体业务和事务处理部分解耦,代码侵入性很低,实际开发中比较常用。我们常用TX和AOP的xml配置文件方式和@Transactional注解方式。
▪声明式事务的优点:
对代码无侵入性,方法内只需要写业务逻辑,节省很多代码量。
▪声明式事务的缺点:
1、声明式事务粒度问题:声明式事务的局限就是最小粒度要作用在方法上,且不适合耗时长、高并发场景。
2、声明式事务容易被开发者忽略,当事务嵌套的方法中存在RPC远程调用、MQ发送、Redis更行、文件写入等操作可能存在以下场景:
▪ 事务嵌套的方法中RPC调用成功了,但是本地事务回滚导致RPC调用无法回滚(暂不讨论分布式事务)。
▪事务嵌套的方法中远程调用会拉长整个事务周期,导致事务的数据库连接一致被占用,类似操作过多会导致数据库连接池耗尽。
3、声明式事务使用错误会导致在某些场景下失效。