聊聊 Spring 注解 @Transactional 失效的那些事 | 京东云技术团队
一、前言
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 注解的事务配置信息。
此方法会检查目标方法的修饰符是否为 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.什么是事务?
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作组成的一个程序执行逻辑单元(Unit)。
通常我们所指的事务是指数据库事务,使用数据库事务有以下两处优点:
当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
事务为数据库操作序列提供了一个从失败恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。
2. 事务具有的特性?
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性,简称事务的 ACID 特性。
原子性
事务的原子性是指事务必须是一个原子的操作序列单元,即事务中包含的各项操作在一次执行过程中只会出现两种状态:全部成功执行、全部不执行。任何一项操作失败都将导致整个事务失败,同时其他已经被执行的操作都将被撤销并回滚,只打所有的操作全部成功,整个事务才算是成功完成。
一致性
事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。也就是说,事务执行的结果必须是使数据库从一个一致性状态转变到另一个一致性状态,因此当数据库只包含成功事务提交 的结果时,就能说数据库处于一致性状态。而如果数据库系统在运行过程中发生故障, 有些事务尚未完成就被迫中断,这些未完成的事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
隔离性
事务的隔离性是指在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。也就是说,不同的事务并发操纵相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的 各个事务之间不能互相干扰。
持久性
事务一旦提交,其所做的修改就会永久保存到数据库中,即使数据库发生故障也不应该对其有任何影响。需要注意的是,事务的持久性不能做到 100%的持久,只能从事务本身的角度来保证永久性,而一些外部原因导致数据库发生故障,如硬盘损坏,那么所有提交的数据可能都会丢失。
3. 什么是 Spring 中的事务?
Spring 中同样提供了很好的事务管理机制,主要分为编程式事务和声明式事务。
编程式事务
是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。编程式事务方式需要开发者在代码中手动的管理事务的开启、提交、回滚等操作。
声明式事务
声明式事务是基于 AOP 面向切面,它将具体业务和事务处理部分解耦,代码侵入性很低,实际开发中比较常用。我们常用 TX 和 AOP 的 xml 配置文件方式和 @Transactional 注解方式。
▪声明式事务的优点:
对代码无侵入性,方法内只需要写业务逻辑,节省很多代码量。
▪声明式事务的缺点:
1、声明式事务粒度问题:声明式事务的局限就是最小粒度要作用在方法上,且不适合耗时长、高并发场景。
2、声明式事务容易被开发者忽略,当事务嵌套的方法中存在 RPC 远程调用、MQ 发送、Redis 更行、文件写入等操作可能存在以下场景:
▪ 事务嵌套的方法中 RPC 调用成功了,但是本地事务回滚导致 RPC 调用无法回滚(暂不讨论分布式事务)。
▪事务嵌套的方法中远程调用会拉长整个事务周期,导致事务的数据库连接一致被占用,类似操作过多会导致数据库连接池耗尽。
3、声明式事务使用错误会导致在某些场景下失效。
四、总结
作者:京东科技 宋慧超
来源:京东云开发者社区
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/000e9d850323f48deecae8333】。文章转载请联系作者。
评论