@Transactional 你真的用对了吗?

简介: 在日常开发中,`@Transactional`注解常用于声明式事务管理,但其原理和使用不当可能引发问题。本文通过一个实际场景探讨了自调用方法时事务不生效的问题,并分析了潜在风险:数据不一致。为解决此问题,提供了三种方案:1) 将方法移动到其他服务类;2) 使用`AopContext.currentProxy()`获取代理对象;3) 通过`ApplicationContext`获取Bean。最终建议尽量避免自调用事务操作,确保数据一致性。

场景

我们在日常开发中几乎经常会使用@Transactional对方法或者接口进行事务管理,毕竟声明式事务这么方便,谁不爱呢,哈哈!但是这个注解的实现原理我们需要搞清楚才能避免“踩坑”。本篇文章就是针对一个使用场景来讨论的,希望可以帮助大家避免在开发中犯下这个错。

1.png

某天我在做自己的需求时,发现有一个同事写的老接口(Ps:此处真的是我有一个同事😶)里的一行代码IDE有个警告提示,本来也没在意,以为就是个格式检查,但是瞄了一眼,发现不对劲,提示的是:

"@Transactional 自调用(实际上是目标对象内的方法调用目标对象的另一个方法)在运行时不会导致实际的事务。"
这和事务扯上关系了,那我当然要好好掰头一下,于是我看了一下他的代码,大概是这样(不能暴露业务代码,我搞个伪代码给大家看看):

@Override
public void updateA(List<Things> thingList) {
   
  ...
  updateB(thingList);
}

在这个代码里,updateA 方法里调用了 updateB 方法,它们在各自的接口上都@Transactional注解,但是这个调用又是在当前同一个类下进行的,所以不会开启新的事务,进而也就有了IDE的这个提示。

了解风险

既然已经发现了这个问题出现的场景,那么我们也需要明确这样使用有什么风险呢?其实从IDE的警告来看就已经十分清晰了,关注最后一句“在运行时不会导致实际的事务”,意思就是有一个方法的事务并不会生效,那么就有数据不一致的风险了。
2.png
可能有同学还是有点懵,我举个例子:假设你有一个事务,它包含两个操作:操作A和操作B。如果操作A成功,但操作B失败,那么操作A的结果将被回滚,但操作B由于声明式事务未生效,即未被事务管理,那么它将不会被回滚,但这样如果两个操作涉及到同一数据的处理就会造成不一致的问题了。

解决方案

当然了,作为一个有点东西的专业开发,我们不能只提出问题,而不解决问题嘛,针对这个场景,我们可以有下面三种方案来解决问题:
3.png

方案一:移动大法好

这里我们不提在controller层注入service、service层注入mapper之类的办法,满足有的同学不想动业务代码这一需求,来解决这一问题。既然问题的根因在于Spring 的事务管理默认是基于代理的,只有通过代理对象调用的方法才会被 Spring 的事务管理器捕获并处理。 那么就把其中一个方法在另一个服务类中声明,然后在当前类中注入并调用。这样,它的调用就会被Spring的事务管理器捕获,并在新的事务中运行。

方案二:巧借AOP

使用AopContext.currentProxy()来获取当前的代理对象,然后通过这个代理对象来调用方法。这样,被调用的方法就会被Spring的事务管理器捕获,并在新的事务中运行。但是,这种方法需要在Spring的配置中启用exposeProxy属性。

(YourService) 
AopContext.currentProxy().updateB(thingList);

方案三:IOC容器来帮你

使用ApplicationContext来获取当前的Bean,然后通过这个Bean来调用方法。这样,被调用的方法就会被Spring的事务管理器捕获,并在新的事务中运行。但是,这种方法需要在你的类中注入ApplicationContext

@Autowired
private ApplicationContext applicationContext;

...

applicationContext.getBean("yourService")).updateB(thingList);

注意

以上三种方法的作用是相同的,都会让每个方法都会被其自己的事务管理。具体来说,当你通过Spring的代理对象或者Bean来调用一个方法时,Spring的事务管理器会为这个方法开启一个新的事务。这个新的事务是独立于当前事务的,也就是说,它有自己的事务边界,可以独立地提交或者回滚。
所以,如果A和B操作都对数据库中的同一行数据进行操作,并且它们是在两个不同的事务中执行的,那么如果B操作失败并回滚,A操作不会被回滚。这可能会导致数据的一致性问题。
因此,最终想要两个方法被一个事务管理,还是得通过在一个方法中调用A和B操作来实现这一点,并在这个方法上使用@Transactional注解。

@Transactional
public void updateData() {
   
    operationA();
    operationB();
}

小结

方法是死的,人是活的。大家还是要根据自己实际的业务场景选择合适的方式,当然,尽量还是避免出现这种自调用的事务操作!

目录
相关文章
|
Java 编译器 数据库
@Transactional中指定rollbackFor,弊端以及不能回滚的时候
@Transactional中指定rollbackFor,弊端以及不能回滚的时候
572 3
|
8月前
|
Java 开发者
SpringBoot整合Flowable【01】- 初识工作流引擎
本文介绍了工作流的基本概念,重点讲解了BPM(业务流程管理)和BPMN 2.0(业务流程建模符号)的关系,以及工作流引擎的发展。BPM是一种管理思想,BPMN是实现该思想的工具。文中还介绍了Flowable、Activiti等主流工作流引擎,并详细说明了流程设计的五种方式,包括FlowableUI、BPMN.js自定义、第三方设计器和代码实现等。最后通过一个请假流程图解释了流程图的组成元素,如事件、连线、任务和网关,帮助读者更好地理解工作流的设计与实现。
691 3
SpringBoot整合Flowable【01】- 初识工作流引擎
|
8月前
|
SQL Java 关系型数据库
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
本文通过电商场景中的库存超卖问题,深入探讨了JVM锁、MySQL悲观锁和乐观锁的实现及其局限性。首先介绍了单次访问下库存扣减逻辑的正常运行,但在高并发场景下出现了超卖问题。接着分析了JVM锁在多例模式、事务模式和集群模式下的失效情况,并提出了使用数据库锁机制(如悲观锁和乐观锁)来解决并发问题。 悲观锁通过`update`语句或`select for update`实现,能有效防止超卖,但存在锁范围过大、性能差等问题。乐观锁则通过版本号或时间戳实现,适合读多写少的场景,但也面临高并发写操作性能低和ABA问题。 最终,文章强调没有完美的方案,只有根据具体业务场景选择合适的锁机制。
276 12
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
|
8月前
|
关系型数据库 MySQL 测试技术
记录一次后端接口抖动的排查过程
某天下午,测试新功能时发现页面接口偶尔变慢,平均十次调用中有三到四次出现3秒以上的延迟。排查了接口、数据库和服务器资源后未发现问题,最终锁定为K8s节点的负载均衡策略导致。测试环境仅有一个公网IP,SLB轮询四个Pod(其中三个为空),造成周期性延迟。移除空Pod后问题解决。
230 5
|
2月前
|
存储 消息中间件 NoSQL
跟着大厂学架构01:如何利用开源方案,复刻B站那套“永不崩溃”的评论系统?
本文基于B站技术团队分享的《B站评论系统的多级存储架构》,解析其在高并发场景下的设计精髓,并通过开源技术栈(MySQL、Redis、Java)复刻其实现。文章深入讲解了多级存储、数据同步、容灾降级等关键设计,并附有完整代码实现,助你掌握大厂架构设计之道。
86 0
|
8月前
|
缓存 网络协议 JavaScript
浏览器输入 URL 后的那些事儿
本文介绍了浏览器输入URL后到页面展示的完整过程,涵盖DNS解析、TCP三次握手、HTTP请求与响应、以及浏览器渲染等关键步骤。首先,浏览器通过DNS解析将域名转换为IP地址;接着,通过TCP三次握手建立连接;随后发送HTTP请求获取网页内容;最后,浏览器解析HTML、CSS并构建渲染树,完成页面渲染。整个过程涉及网络协议和浏览器内部机制的协同工作。
243 13
|
6月前
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
355 1
Java 中数组Array和列表List的转换
|
8月前
|
存储 Java API
SpringBoot整合Flowable【02】- 整合初体验
本文介绍了如何基于Flowable 6.8.1版本搭建工作流项目。首先,根据JDK和Spring Boot版本选择合适的Flowable版本(7.0以下)。接着,通过创建Spring Boot项目并配置依赖,包括Flowable核心依赖、数据库连接等。然后,建立数据库并配置数据源,确保Flowable能自动生成所需的表结构。最后,启动项目测试,确认Flowable成功创建了79张表。文中还简要介绍了这些表的分类和常用表的作用,帮助初学者理解Flowable的工作原理。
1607 0
SpringBoot整合Flowable【02】- 整合初体验
|
8月前
|
存储 Java 数据安全/隐私保护
SpringBoot整合Flowable【03】- 通过Flowable-UI体验一个简单流程
本文介绍了如何使用Flowable 7.0以下版本的flowable-ui进行流程建模、发布和执行。首先,通过解压并启动flowable-ui war包,访问http://localhost:8080/flowable-ui/idm/#/login登录系统。接着,创建并绘制一个简单的绩效流程模型,包含开始节点、任务节点(自评、上级评、隔级评)和结束节点,并为各节点分配处理人。然后,创建应用并发布绩效流程。最后,通过创建a、b、c三个用户分别完成各节点任务,演示了整个流程的执行过程。本文旨在帮助读者理解Flowable的基本操作和流程元素,后续将介绍通过Java代码控制流程的方法。
1451 1
SpringBoot整合Flowable【03】- 通过Flowable-UI体验一个简单流程
|
9月前
|
Java 开发者 Spring
理解和解决Spring框架中的事务自调用问题
事务自调用问题是由于 Spring AOP 代理机制引起的,当方法在同一个类内部自调用时,事务注解将失效。通过使用代理对象调用、将事务逻辑分离到不同类中或使用 AspectJ 模式,可以有效解决这一问题。理解和解决这一问题,对于保证 Spring 应用中的事务管理正确性至关重要。掌握这些技巧,可以提高开发效率和代码的健壮性。
606 13