详解 Spring 事务的传播机制

 2022-09-06
原文地址:https://blog.csdn.net/szy350/article/details/122466050

目录

一、事务在Spring中是如何运作的

1.1 开启事务(DataSourceTransactionManager.doBegin)

二、Spring的事务传播机制

2.1 子事务的传播机制为REQUIRED

2.2 子事务的传播机制为REQUIRES_NEW

2.3 子事务的传播机制为NESTED

当我们在使用Spring所提供的事务功能时,如果是仅仅处理单个的事务,是比较容易把握事务的提交与回滚,不过一旦引入嵌套事务后,多个事务的回滚和提交就会变得复杂起来,各个事务之间是如何相互影响的,是一个值得讨论的点。

一、事务在Spring中是如何运作的

在了解嵌套事务之前,可以先看下单个事务在Spring中的处理流程,以便后面可以更清晰地认识嵌套事务的逻辑。

202209062321541271.png

Spring事务使用AOP的机制实现,会在@Transactional注解修饰的方法前后分别织入开启事务的逻辑,以及提交或回滚的逻辑。@Transactional可以修饰在方法或者类上,区别就在于修饰于类上的,会对该类下符合条件的方法(例如private修饰的方法就不符合条件)前后都织入事务的逻辑。

具体的处理逻辑如下(具体的方法路径为TransactionInterceptor.invoke -> TransactionAspectSupport.invokeWithinTransaction):

1.1 开启事务(DataSourceTransactionManager.doBegin)

202209062321553212.png

这里主要做了获取连接,并关闭自动提交,将@Transactional注解中的一些参数初始化到txObject对象中。

1.2 异常回滚(TransactionAspectSupport.completeTransactionAfterThrowing)

202209062321566763.png

这里是事务异常回滚的地方,这里有个注意点是回滚会先用rollbackOn这个方法判断,默认情况下只有RunTimeException以及Error是会进行回滚的,除非在@Transactional显式声明了rollbackFor。

202209062321579264.png

二、Spring的事务传播机制

当出现多个事务嵌套的场景发生时,Spring事务的处理会变得复杂一些,需要考虑嵌套事务下的提交顺序,以及回滚顺序。对此,Spring提供了多种的传播机制,每种传播机制的效果都不尽相同,以便应对各种复杂的业务场景。

正常处理的嵌套事务流程如下:

202209062321589725.png

传播机制以及它们的效果如下:

REQUIRED:默认值,支持当前事务,如果没有事务会创建一个新的事务

SUPPORTS:支持当前事务,如果没有事务的话以非事务方式执行

MANDATORY:支持当前事务,如果没有事务抛出异常

REQUIRES_NEW:创建一个新的事务并挂起当前事务

NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则将当前事务挂起

NEVER:以非事务方式进行,如果存在事务则抛出异常

NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作

以下的说明均建立在父事务的传播机制为REQUIRED的前提下,来探讨各种传播机制的回滚策略。

2.1 子事务的传播机制为REQUIRED

子事务 主事务 结果
子事务 主事务 结果
异常 正常,并try-catch异常 均回滚
正常 异常 均回滚
正常 异常,并try-catch异常 不回滚

这里详细说下第一种场景,子事务异常情况下,主事务捕获了子事务的异常却仍发生了回滚。从代码来看:

202209062322000326.png

回滚的原因在于,子事务失败的时候在回滚代码中设置了全局回滚的标识(AbstractPlatformTransactionManager.processRollback)。

202209062322013477.png

之后主事务在进行事务提交时,会判断全局回滚标识是否存在。若存在就会进行回滚动作。(AbstractPlatformTransactionManager.commit)

202209062322023728.png

2.2 子事务的传播机制为REQUIRES_NEW

子事务 主事务 结果
子事务 主事务 结果
异常 正常,并try-catch异常 子回滚,主不回滚
正常 异常 子不回滚,主回滚
异常 正常 均回滚

这里主要说下第三种场景,因为从REQUIRES_NEW的描述中容易造成误解:创建一个新的事务并挂起当前事务,很容易理解为子事务独立于主事务,子事务回滚后不会影响主事务的执行。

我认为问题点主要在于对挂起的认识,可以看下Spring时如何执行的(AbstractPlatformTransactionManager.handleExistingTransaction):

202209062322038569.png

在这个挂起动作中主要做了两件事,一是将全局ThreadLocal中的配置初始化,二是将原事务的信息保存在SuspendedResourcesHolder对象中。

当子事务异常回滚后,就会通过AbstractPlatformTransactionManager.resume方法恢复主事务。

2022090623220491210.png

恢复后,子会再向外抛出一个异常,因此主事务会接收到该异常,因此主事务也发生回滚。

2022090623220619711.png

总的来说,挂起的动作并不代表子完全独立于主。

2.3 子事务的传播机制为NESTED

子事务 主事务 结果
子事务 主事务 结果
异常 正常,并try-catch异常 子回滚,主不回滚
正常 异常 均回滚
异常 正常 均回滚

NESTED是通过rollback的savepoint机制,实现异常回滚。相比于子事务为REQUIRED下,子事务异常,主事务try-catch了异常,REQUIRED情况下主子均会回滚,但NESTED模式主不会回滚。因此可以看下场景一,NESTED是如何处理的。

子事务如果是NESTED模式,则会保存savepoint,以便后面回滚到该savepoint。

回滚时,直接回滚到savepoint,且不会设置全局回滚标识。因此即相当于就是回滚了子事务。主事务由于try-catch了异常,因此执行方法的时候也没有抛出异常,正常走提交流程,且没有全局回滚标识,故不会回滚,正常提交。

PS: mysql的savepoint机制

BEGIN;

INSERT INTO test_entity values (1, 'aaa', 10); ①

SAVEPOINT savepoint1;

INSERT INTO test_entity values (2, 'bbb', 11); ②

ROLLBACK TO savepoint1;

RELEASE SAVEPOINT savepoint1;

COMMIT;

回滚到savepoint1,①会入库,②被回滚。