场景
我们在日常开发中几乎经常会使用@Transactional对方法或者接口进行事务管理,毕竟声明式事务这么方便,谁不爱呢,哈哈!但是这个注解的实现原理我们需要搞清楚才能避免"踩坑"。本篇文章就是针对一个使用场景来讨论的,希望可以帮助大家避免在开发中犯下这个错。
某天我在做自己的需求时,发现有一个同事写的老接口(Ps:此处真的是我有一个同事😶)里的一行代码IDE有个警告提示,本来也没在意,以为就是个格式检查,但是瞄了一眼,发现不对劲,提示的是:
"@Transactional 自调用(实际上是目标对象内的方法调用目标对象的另一个方法)在运行时不会导致实际的事务。"
这和事务扯上关系了,那我当然要好好掰头一下,于是我看了一下他的代码,大概是这样(不能暴露业务代码,我搞个伪代码给大家看看):
java
@Override
public void updateA(List<Things> thingList) {
...
updateB(thingList);
}
在这个代码里,updateA
方法里调用了 updateB
方法,它们在各自的接口上都@Transactional
注解,但是这个调用又是在当前同一个类下进行的,所以不会开启新的事务,进而也就有了IDE的这个提示。
了解风险
既然已经发现了这个问题出现的场景,那么我们也需要明确这样使用有什么风险呢?其实从IDE的警告来看就已经十分清晰了,关注最后一句"在运行时不会导致实际的事务"
,意思就是有一个方法的事务并不会生效,那么就有数据不一致的风险了。
可能有同学还是有点懵,我举个例子:假设你有一个事务,它包含两个操作:操作A和操作B。如果操作A成功,但操作B失败,那么操作A的结果将被回滚,但操作B由于声明式事务未生效,即未被事务管理,那么它将不会被回滚,但这样如果两个操作涉及到同一数据的处理就会造成不一致的问题了。
解决方案
当然了,作为一个有点东西的专业开发,我们不能只提出问题,而不解决问题嘛,针对这个场景,我们可以有下面三种方案来解决问题:
方案一:移动大法好
这里我们不提在controller层注入service、service层注入mapper之类的办法,满足有的同学不想动业务代码这一需求,来解决这一问题。既然问题的根因在于Spring 的事务管理默认是基于代理的,只有通过代理对象调用的方法才会被 Spring 的事务管理器捕获并处理。 那么就把其中一个方法在另一个服务类中声明,然后在当前类中注入并调用。这样,它的调用就会被Spring的事务管理器捕获,并在新的事务中运行。
方案二:巧借AOP
使用AopContext.currentProxy()
来获取当前的代理对象 ,然后通过这个代理对象来调用方法。这样,被调用的方法就会被Spring的事务管理器捕获,并在新的事务中运行。但是,这种方法需要在Spring的配置中启用exposeProxy
属性。
java
(YourService)
AopContext.currentProxy().updateB(thingList);
方案三:IOC容器来帮你
使用ApplicationContext
来获取当前的Bean ,然后通过这个Bean来调用方法。这样,被调用的方法就会被Spring的事务管理器捕获,并在新的事务中运行。但是,这种方法需要在你的类中注入ApplicationContext
。
java
@Autowired
private ApplicationContext applicationContext;
...
applicationContext.getBean("yourService")).updateB(thingList);
注意
以上三种方法的作用是相同的,都会让每个方法都会被其自己的事务管理。具体来说,当你通过Spring的代理对象或者Bean来调用一个方法时,Spring的事务管理器会为这个方法开启一个新的事务。这个新的事务是独立于当前事务的,也就是说,它有自己的事务边界,可以独立地提交或者回滚。
所以,如果A和B操作都对数据库中的同一行数据进行操作,并且它们是在两个不同的事务中执行的,那么如果B操作失败并回滚,A操作不会被回滚。这可能会导致数据的一致性问题。
因此,最终想要两个方法被一个事务管理,还是得通过在一个方法中调用A和B操作来实现这一点,并在这个方法上使用@Transactional
注解。
java
@Transactional
public void updateData() {
operationA();
operationB();
}
小结
方法是死的,人是活的。大家还是要根据自己实际的业务场景选择合适的方式,当然,尽量还是避免出现这种自调用的事务操作!