Spring Transaction 注解不生效bug引发的思考

前言

某日,在项目测试代码过程中,发现一个问题,对于一个方法A(无事务),调用B方法(有事务),当A,B方法在同一个类中的时候,在B方法上的事务注解是不生效的!

同事说将B方法写到新的Service类中就可以解决,遂试之,确实得以解决。但不解其原理,问同事、查资料均感觉不如意。故分析了下Spring 事务的部分源码。有所见解,特此记录。

下图就是我描述的这种情况,B事务不生效的问题。

1.测试类

upload successful

2.实现类

upload successful

我们经过测试可以发现,当insert方法有事务、但被该实现类内部方法doInsert调用后,即使insert方法出现异常,该方法的数据库操作也不会回滚。

3.数据没有回滚,已经入库

upload successful

正文

要理解研究这种情况,我们先来简单说下Spring 的注解方式的事务实现机制。

事务的一些基础我在一篇文章中有介绍 https://www.sakuratears.top/blog/Spring-Transactional%E6%B3%A8%E8%A7%A320181013/ 不懂得可以先大致看看。

Spring注解方式的事务实现机制

在应用系统调用声明@Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据@Transactional 的属性配置信息,这个代理对象决定该声明@Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务, 如图。

upload successful

Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,上图以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法。当然我们也可以使用AspectJ的方式实现AOP代理,这儿不做过多介绍。

事务管理的框架是由抽象事务管理器 AbstractPlatformTransactionManager 来提供的,而具体的底层事务处理实现,由 PlatformTransactionManager 的具体实现类来实现,如事务管理器 DataSourceTransactionManager。不同的事务管理器管理不同的数据资源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。

PlatformTransactionManager,AbstractPlatformTransactionManager 及具体实现类关系图如下。

upload successful

一次正常事务调试

出现问题,debug是比较好的解决方法。我们大致跟下SpringTransaction的使用过程。先从正确流程入手吧。
如下:直接将事务注解加在doInsert方法上,明显,这种情况下出现异常事务会回滚。我们debug下事务大致的回滚过程。

upload successful

DefaultAopProxyFactory里的createAopProxy方法可以拿到看到该方法具体使用的哪种代理。

upload successful

可以看到我们这个类使用了Cglib代理。
使用了Cglib代理,上面讲到 对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。我们继续断点跟踪下。

upload successful

一步步进行,如事务图所示,进入了TransactionInterceptor的invoke方法,并执行invokeWithinTransaction方法。

upload successful

继续跟踪。来到了TransactionAspectSupport,这是spring事务处理的关键类,谨记。

upload successful

upload successful

会进行事务的创建,createTransactionIfNecessary getTransaction方法会开启一个事务。

upload successful

根据上面debug看到的事务管理器是DataSourceTransactionManager, 执行getTransaction会调用它的doBeigin方法。

upload successful

upload successful

可以看到把自动提交设置成了false,并且暂时保存了原来的自动提交属性状态。

upload successful

而后可以看到他将当前事务信息绑定在了ThreadLocal里了。

upload successful

执行我们添加事务注解的方法,抛出了异常被捕获。

upload successful

执行completeTransactionAfterThrowing方法,我们的异常正好是这个异常(或者其父类)。

upload successful

然后执行回滚操作,最终到达下图所示方法(DataSourceTransactionManager的doRollback)

upload successful

回滚具体代码不在介绍,我们可以看到在回滚时它把原来数据库的自动提交属性改了过来。

upload successful

最后他会把本次事务状态清除,相当于保存上一次的事务状态。

upload successful

注意:

  1. 在spring启动时获取事务注解时我们可以看到下图

    upload successful

    这个说明Spring AOP 事务注解只能作用于public 方法。

  2. 关于事务回滚rollbackFor条件的问题,我们可以看到下图

    upload successful

    当我们事务注解配置具体的回滚条件,如rollbackFor = Exception.class,只要是Exception.class或者其子类,都可以实现事务回滚。它会通过RuleBasedTransactionAttribute.class这个类去校验抛出的异常是否符合条件。进而判断是否需要回滚。

    但是当我们不声明rollbackFor 条件时,这儿应该注意一点。它会使用默认的条件,而不是不处理异常。主要由DefaultTransactionAttribute.class 里的rollbackOn方法实现。我们看下这个方法,可以发现,他只会处理RuntimeException和Error。也就是说,如果我们一个方法有事务,但抛出了非RuntimeException(如检查时异常等),且事务没有声明rollbackFor回滚条件,那么,它是不会触发事务回滚的。这一点要注意。

    upload successful

    上图调用RuleBasedTransactionAttribute.class的方法,回滚规则为空,使用父类rollbackOn方法。

    upload successful

    upload successful

    通过调试可以轻松看到这一情况,这儿不在做过多赘述。

异常事务调试

我们来看下事务不成功的情况。就是题目开始的问题。

Spring刚启动时,会扫描需要进行代理的类,生成代理对象,在AdvisedSupport.class类中,把类中的方法缓存起来。

upload successful

首先查询该方法是不是需要拦截(是不是有事务注解)

upload successful

upload successful

在TransactionAttributeSourcePointcut.class 类里的matches方法,查询事务注解情况。

upload successful

查到了就缓存起来了。

当doInset方法进入时,同样的逻辑。也会缓存起来,但是cached是值为null。

upload successful

upload successful

该类的其他方法也会被缓存,没有事务注解的都放为null。

开始执行doInsert方法时,进入CglibAopProxy的intercept方法。

upload successful

可以看到尝试拿缓存,但缓存的值为空。

upload successful

尝试获取一下,显然也是没有值的。所以这时候认为不需要进行事务。事务链为空。

upload successful

就直接执行了方法doInsert。并不会开启事务。(不为空的话会创建一个CglibMethodInvocation并开启事务执行方法,如上面开始的情况)。

当生成一个动态代理对象后,对这个对象引用中方法的调用就是对代理的调用,而这个代理能够代理所有跟特定方法调用相关的拦截器。不过,一旦调用最终抵达了目标对象 (此处为TransactionalTestImpl类的引用),任何对自身的调用例如insert将对this引用进行调用而非代理。这一点意义重大, 它意味着自我调用将不会导致和方法调用关联的通知得到执行的机会。

如果需要insert的事务生效,一种典型的方法就将方法insert放到新的类中,这便很好理解了。因为新的类会生成新的动态代理对象,调用源从而获得通知。

如果我非要在本类中实现通知呢?

那我们就需要直接获取代理对象调用insert方法了。如下图。

upload successful

要实现这个功能,需要开启Spring AspectJ支持,我使用的Springboot,启动类上加入如下注解,并引入如下依赖。

upload successful

upload successful

这个pom文件你进去可以看到就是引用了AspectJ 的相关jar包。

upload successful

这个时候我们在测试一下,就会发现事务生效了。

在CglibAopProxy中可以看到如下代码,可以明白开启后它把代理对象绑定到ThreadLocal上等待insert方法执行的通知。

upload successful

upload successful

upload successful

当然,如果这两个方法上都存在事务,它也会进行判断处理,也就是事务的传播属性,他们主要通过AbstractPlatformTransactionManager这个类(这个类也很重要)的getTransaction方法和handleExistingTransaction方法来进行事务传播属性的处理。这儿不做过多讲解,自己看看逻辑处理即可。

getTransaction部分代码:

upload successful

handleExistingTransaction部分代码:

upload successful

结语

总的来说,通过一个问题,我们大致看了下Spring Transactional注解的实现过程。并分析了产生这种问题的原因,通过有效的手段来进行验证。还是蛮不错的一次体验。

下面总结下:

A:在Spring中,一个类中无事务注解的方法A调用有事务注解的方法B,默认情况下B出现异常事务是不会进行回滚的

解决方法:

  1. 将B写到一个新的方法中。(原理上是生成不同类的动态代理对象,实际中比较常用的一种手段,但需要管理一个新的类)

  2. 如果业务(情形等)允许,可以将事务移动到A上,或者B的事务不动,给A也加一个事务。(根据具体情况讨论,有时候效果很好,有时候不适宜,使用此种方法可能影响程序效率或者产生莫名其妙的bug,慎用)

  3. 启用增强型事务,引入AspectJ。(不太常用的一种手段,但如果项目中本来已经引入了AspectJ并且开启了增强型事务管理,何乐而不为呢?)

B:研究过程中发现的其他应该注意的坑

  1. 事务注解应当作用在public方法上,需要注意

  2. 如果不设置事务回滚条件(rollbackFor参数为空),它能捕获RuntimeException及其子类 和 Error及其子类 出现的异常情况并回滚,其他异常是无法捕获并回滚的。如IOException(检查型异常)等

  3. 事务的传播属性的几个应该了解,不能乱用,虽然我们可能就用到过或者就用到了Propagation.REQUIRED ,但不代表其它不会用到

C:本次研究学习我们应该理解掌握的

  1. Spring事务的处理过程。(Spring AOP的体现,应用反射和动态代理)

  2. 事务的一些性质。(事务的传播属性、事务的四大特性等)

  3. 其它一些需要学习的地方。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道