一、前言

emm,又又又踩坑啦。这次的需求主要是对逾期计算的需求任务进行优化,现有的计算任务运行时间太长了。简单描述下此次的问题:在项目中进行多个数据库执行操作时,我们期望的是将其整个封装成一个事务,要么全部成功,或者全部失败,然而在自测异常场景时发现,里面涉及的第一个数据状态更新成功了,但是后面的数据在插入出现异常,后面查询数据表发现,该数据的状态已经被更新成功啦

聊聊Spring注解@Transactional失效的那些事-LMLPHP

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方法或JDKDynamicAopProxyinvoke方法会间接调用AbstractFallbackTransationAttributeSourcecomputeTransactionAttribute方法,获取@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主要功能如下:执行两次数据库插入操作,并在扩展信息字段中添加备注;

聊聊Spring注解@Transactional失效的那些事-LMLPHP

▪ 运行结果如下,构造的单号不存在订单查询为空触发异常,观察数据库发现,第一次数据库插入操作已经执行成功,故而验证@Transactional注解失效

聊聊Spring注解@Transactional失效的那些事-LMLPHP

聊聊Spring注解@Transactional失效的那些事-LMLPHP

  • @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方法;观察下多个插入操作是否会因为异常而中断回滚;

聊聊Spring注解@Transactional失效的那些事-LMLPHP

▪ 运行结果如下,还是通过构造的单号不存在订单查询为空触发异常,观察数据库发现,第一次数据库插入操作已经执行成功第二次数据插入操作失败,并没有因为异常而触发事务操作,故而验证@Transactional注解方法间的调用会失效

聊聊Spring注解@Transactional失效的那些事-LMLPHP

聊聊Spring注解@Transactional失效的那些事-LMLPHP

  • 多线程任务可能导致@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方法中对异常进行捕获;观察下多个插入操作是否会因为异常而中断回滚;

聊聊Spring注解@Transactional失效的那些事-LMLPHP

▪ 运行结果如下,还是通过构造的单号不存在订单查询为空触发异常,但是我们在方法内部对该异常进行捕获,并未向上层抛出,我们期望的场景是两次数据插入执行失败,但是观察数据库发现,第一次数据库插入操作已经执行成功第二次数据插入执行成功,与我们的预期结果不符,故而验证@Transactional注解在方法中异常被捕获的场景中失效

聊聊Spring注解@Transactional失效的那些事-LMLPHP

聊聊Spring注解@Transactional失效的那些事-LMLPHP

究其原因: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、声明式事务使用错误会导致在某些场景下失效

四、总结

聊聊Spring注解@Transactional失效的那些事-LMLPHP

07-18 10:22