@Transactional失效问题

2023-12-13 20:59:56
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

关于@Transactional

日常做项目时,一般情况下Service方法中如果有多个增删改方法的调用,我们会在该业务方法上加@Transactional从而保证事务的执行(SpringBoot自动装配默认开启事务管理,无需@EnableTransactionManagement):

这段代码没太多意义,就是更新一个User的同时,更新另一个。

@Transactional注解有多个属性可以设置,实际开发中比较常用的有两个:

  • propagation:用于指定事务传播行为
  • rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型

这篇文章还不错,可以看完后再回来:总结6种@Transactional注解的失效场景

对于propagation属性,Spring提供了一个枚举类方便我们指定事务传播行为的类型:

特别注意,@Transactional默认的事务传播行为是Propagation.REQUIRED,所以上面的updateUser()我只指定了rollbackFor。

上面文章提到的6种情况里,一般来说可能犯错误的就以下2种:

  • 同一个类中方法调用,导致@Transactional失效
  • 异常被你的catch“吃了”导致@Transactional失效

对于第2种情况,我的处理办法是尽量不在Service层直接try catch,而是习惯抛出业务异常,让@RestControllerAdvise统一捕获并返回给前端。

但对于第1种情况,怎么处理呢?毕竟实际开发中,有时确实可能一不小心就发生同一个类的方法互调,此时如何解决事务失效问题呢?

发现问题

请观察下方截图中的代码,不用在意具体的上下文:

  • selectUser()不加事务控制,但调用了updateUser()
  • updateUser加了事务控制,调用了两次userMapper.update(),中间会抛出“除零异常”

selectUser()不够贴切,名字随便取的,请把它当做一个没有事务的增删改方法

在test方法中调用:

测试前数据库记录:

测试结果:

这证明了同一个类中的非事务方法调用事务方法确实会导致事务失效(如果事务没失效,应该会回滚,16不会被修改)。

解决问题

方法1:给selectUser()加上@Transactional

事务确实控制住了:

方法2:ApplicationContext获取代理对象

同一个类中非事务方法调用事务方法导致事务失效的根本原因在于,非事务方法中调用updateUser()本质上就是this.updateUser(),而this并不是代理对象,而是普通对象(后面再解释)。

知道原因后就很好解决了:

先在selectUser()内部获取UserService的代理对象,再通过代理对象调用updateUser()即可

方法3:注入自身

由于Spring已经替我们解决了循环依赖的问题,所以AService可以注入AService自身。

比如:

@Service
public class UserServiceImpl implements UserService {
	@Autowired
    private UserService userService
}

方法4:AopContext.currentProxy()获取代理对象

原理同上,本质是也是在selectUser()方法中获取代理对象。不过这个方法需要额外做2步:

  • 引入aop依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 添加注解

AopContext可以通过当前线程ThreadLocal得到代理对象。

关于代理对象与this

最后分别解释一下上面三种办法为什么能解决事务失效的问题,其中方法2和3的原理是一样的。

先看方法1:给selectUser()加上@Transactional

我们原先观察问题的角度是:selectUser()调用updateUser(),会导致updateUser()事务失效。一般来说,正向思维是想办法让updateUser()事务起效,但方法1却采用了逆向思维:让selectUser()的事务起效,从而把updateUser()放在一个更大的事务中,最终控制事务。

也就是说,它并没有解决updateUser()事务失效的问题,内部其实还是this.updateUser(),是普通方法调用。之所以最终看起来好像事务控制成功,是因为updateUser()内部的异常沿着方法调用链向上抛,到了selectUser()这里触发了回滚。

讲完了方法1起效的本质后,我们再来聊聊为什么userService.selectUser()在调用时明明是代理对象:

怎么到了selectUser()内部时,this就成普通对象了呢:

请注意,即使我现在在selectUser()上加了@Transactional注解,里面的this还是普通对象。也印证了我上面的观点:方法1并没有解决updateUser()事务失效的问题,因为它还是用this普通对象调用updateUser(),并不会触发事务控制。

总而言之,此时this != userService。是不是觉得很不可思议?

Why?

这要从动态代理的底层原理说起(请参考之前动态代理相关的文章),简而言之就是下面这幅图:

动态代理的原理是,我们可以在InvocationHandler的invoke()方法中使用target目标对象调用目标方法,最终得到的效果和静态代理是一样的:

所以在add()方法里使用this,其实得到的是target,也就是目标对象,而不是代理对象。

Spring自动注入时,其实是把代理对象注入到每一个@Autowired private UserService userService中。我们在Controller调用userService代理对象的add()方法时,最终会转到目标对象的add()方法。

讲完上面方法1的原理,方法2和方法3就无需多言了吧。只不过方法3得到代理对象的方式有点奇特:

最后的最后,在讨论事务控制是否起效时,本文的一切论点都是基于以下2点:

  • 首先,要是代理对象
  • 其次,方法上要有@Transactional(或者xml配置形式)

至于为什么代理对象的方法上加了@Transactional就会触发事务,需要去看Spring的AOP源码,里面涉及到了责任链模式和递归算法。大体思路是:

0.在Spring AOP的世界里,一个个增强方法(增强代码)会被包装成一个个拦截器,放在拦截器链中。

1.代理对象调用每个方法时,其实最终都会被导向一个叫CglibAopProxy.intercept()的方法,而这个方法会判断当前方法有没有需要执行的拦截器链chain。

简单来说就是:

// 获取拦截器链

if(chain.isEmpty() && Modifier.isPublic(method.getModifiers())){
    // 执行目标方法
} else {
    // 走拦截器链...
}

点进去else分支的代码,会看到:

“方法为public”时才会返回methodProxy,也能被代理。也验证了@Transactional失效的另一个情况:方法不为public时,@Transactional失效。

2.当public方法加了@Transactional,事务控制的代码就会被加入到拦截器链中,最终就会出现在事务方法的前后调用。

特别要注意,任何Java代码层面的事务控制其实还是依赖于setAutoCommit(false),也就是先关闭默认提交,此时MySQL底层就会通过日志把一连串操作先记录起来,最后一起提交。如果中间失败了,仍可根据日志回滚。具体实现细节可以去查阅MySQL事务相关资料。

另外大家可以关注下上面invokeWithinTransaction()的第二行代码,里面有一句

tas.getTransactionAttribute(method, targetClass)

本质就是传入当前事务方法和Class对象,读取上面@Transactional的注解属性,比如我们对rollbackFor和propagation的设置。

然后再往下会调用

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

传入一些参数判断决定是否真的开启事务(名字很形象,createTransactionIfNecessary),如果我们没有使用@Transactional,就不会开启事务了。

重新理解rollbackFor和propagation

相信大家以前也看了很多类似的文章,但是看完就忘了。既然花了时间,肯定还是希望能一劳永逸。所以本文也不打算这么蜻蜓点水般结束,而是来个回马枪,和大家一起重新看看这两个属性,相信理解会更深刻。

先说结论:

  • 并不是所有的异常都会触发事务回滚,所以最好指定rollbackFor(一般图省事都直接指定Exception.class)
  • propagation是写给调用者看的,而不是写给被调用者看的(一句话解释有点晦涩,后面展开)

最好指定rollbackFor

我们来看看rollbackFor的注释:

也即是说,虽然rollbackFor默认指定了异常类型,但仅仅包括Error和RuntimeException。如果是其他自定义的业务异常,就不会触发回滚(理论上是这样,但通常业务异常都会继承自RuntimeException,因为运行时异常无需强制处理)。

propagation的案例

接下来结合上面的selectUser(),我们来看看propagation每种情况的具体演示。

Propagation.REQUIRED

如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务

selectUser()和updateUser()都加上事务控制时,虽然内部调用还是this.updateUser(),是普通方法调用,但整体上在selectUser()的事务中。

Propagation.SUPPORTS

如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

事务失效了。

原因是test方法调用userService.selectUser()时,本身是没有事务的,而刚好selectUser()使用了SUPPORT:当前存在事务,则加入事务;如果不存在事务,则以非事务方式继续运行。

这里所谓的当前,其实就是指调用方,即调用selectUser()的方法是否存在事务。由于test不存在事务,于是selectUser()也就没有事务,而this.updateUser()本身事务失效,所以最终整个调用事务失效。

如果希望selectUser()事务起效,SUPPORTS的情况下,可以给调用方加@Transactional:

Propagation.MANDATORY

mandatory:强制的。

如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。也就是要求调用方必须存在事务。

同理,给test方法加上事务,那么selectUser()就会处于test的事务中,不会抛异常。

看到这里,大家是不是同意本小节开头说的那句话了呢:

propagation是写给调用者(test)看的,而不是写给被调用者(updateUser)看的

Propagation.REQUIRES_NEW

重新创建一个新的事务,和外面的事务相互独立。

比如:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
methodA(){
    // 1.插入a表
    ...
    // 2.调用methodB
    methodB();
    // 3.在methodA抛异常,回滚
    int i = 1/0;
}

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
methodB(){
    // 4.插入b表
}

methodA抛异常了,回滚了,但是methodB还是会插入记录。因为methodB是REQUIRES_NEW,自己起了一个事务。也就是说,methodA和methodB各管各的,无论是谁的内部抛异常都不会影响外部回滚。

Propagation.NOT_SUPPORTED

以非事务的方式运行,无论调用者是否存在事务,自己都不受其影响。和Propagation.REQUIRES_NEW有点像,但NOT_SUPPORTED自己是没有事务的。

Propagation.NEVER

以非事务的方式运行,如果当前存在事务,则抛出异常。即如果methodB设置了NEVER,而methodA设置了事务,那么调用methodB时就会抛异常。它不想在有事务的方法内运行。

Propagation.NESTED

和Propagation.REQUIRED效果一样。

最后说一句,我平时就看过第一、第二种。99%情况下都是默认REQUIRED,只需注意rollbackFor即可。

本文讨论是同类内的非事务方法调用事务方法,而不是调用其他类的事务方法,那和代理对象调用没区别。

@Service
class UserServiceImpl implements UserService {
    @Autowired
    private StudentService studentService;
    
    public void methodA(){
        // 方法内部的一些操作
        ...
            
        // 调用同类的methodB()
        methodB();
        
        // 调用StudentService的方法
        studentService.methodC();     
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void methodB(){
        
    }
}

另外,大家以前可能在各种平台看过@Async注解也存在同类方法调用失效的问题。看完这篇文章,你觉得是为什么呢~

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

文章来源:https://blog.csdn.net/smart_an/article/details/134940805
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。