Spring事务原理解析

1,856 阅读11分钟

版本基于Spring5.3.8

基础知识介绍

在正式介绍SpringTx的源码之前,先来回顾下基础知识

Spring事务传播机制类型

此小段从segmentfault.com/a/119000002…

PROPAGATION_REQUIRED (默认)

  • 支持当前事务,如果当前没有事务,则新建事务
  • 如果当前存在事务,则加入当前事务,合并成一个事务

REQUIRES_NEW

  • 新建事务,如果当前存在事务,则把当前事务挂起
  • 这个方法会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交

NESTED

  • 如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父事务结束才提交
  • 如果当前没有事务,则新建事务
  • 如果它异常,父级可以捕获它的异常而不进行回滚,正常提交
  • 但如果父级异常,它必然回滚,这就是和 REQUIRES_NEW 的区别

SUPPORTS

  • 如果当前存在事务,则加入事务
  • 如果当前不存在事务,则以非事务方式运行,这个和不写没区别

NOT_SUPPORTED

  • 以非事务方式运行
  • 如果当前存在事务,则把当前事务挂起

MANDATORY

  • 如果当前存在事务,则运行在当前事务中
  • 如果当前无事务,则抛出异常,也即父级方法必须有事务

NEVER

  • 以非事务方式运行,如果当前存在事务,则抛出异常,即父级方法必须无事务

案例

案例一

外层事务:REQUIRED

@Service
public class TestServiceImpl implements TestService {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private TestService2 testService2;

    @Override
    @Transactional
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')");
        testService2.test();
        throw new RuntimeException();
    }
}

内层事务:REQUIRES_NEW

@Service
public class TestServiceImpl2 implements TestService2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')");
        return "2";
    }

}

运行结果

外层写入数据失败,内层写入数据成功

案例二

外层事务不变,内层事务变成NESTED

内层事务:NESTED

@Service
public class TestServiceImpl2 implements TestService2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')");
        return "2";
    }

}

运行结果

内外层全部写入失败

源码分析

在案例分析开始之前,我们先确定下源码剖析的入口,众所周知,SpringAOP是基于动态代理实现的,我们根据这个可以轻松确定到Spring事务的核心入口,TransactionInterceptor,对于interceptor,当然是看他的invoke方法了

image.png

可以从invoke方法中找到核心的逻辑也就是invokeWithinTransaction,这个便是SpringTx的具体实现入口了。

源码分析我们先从案例一入手,首先代码进入的是外层的事务。从入口开始分析,invokeWithinTransaction分为几个核心的部分,分别如下

img

注意这里第三步是completeTransactionAfterThrowing,因为外层事务是抛出异常的。这几个步骤首先看第一个createTransactionIfNecessary,他的作用是获得一个

TransactionInfo,这个对象是贯穿整个Spring事务的核心对象,请务必记住这个名字,那么这个对象中有什么核心属性呢?注意这里并没有列出全部的属性

img

这里有两个属性,一个是TransactionStatus对象,一个是oldTransactionInfo,具体是做什么用呢,我们暂且按下不表。先看createTransactionIfNecessary中的逻辑便知。

img

首先第一个步骤是getTransaction,见名知意,这是获得事务对象的方法,那么从何处获得呢?分为两个步骤

img

首先是doGetTransaction

img

doGetTransaction其中核心的点是doGetResource方法,在这个方法中的核心逻辑是:调用resources的get方法获得一个映射,再取出这个映射中的value

org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource

image.png

那么这里会衍生出来几个核心问题

  1. resources是什么?
  2. 取出的这个map,key和value又分别是什么

查看源码,可以看到resources是一个Threadlocal,存储的对象是一个Map,那么这个map又是什么呢

image.png

我们如果自行Debug源码,可以看到

  • key是Datasource对象
  • value是一个ConnectionHolder对象

ConnectionHolder对象中的核心参数如下

img

因此,总结一下,这个resources其实是以线程为维度,存储一个数据源对象和链接对象的映射。那么是否此时的调用doGetResource的结果就是这个ConnectionHolder呢?回到我们的案例中,此时是外层第一次调用,因此答案是否定的,因为此时线程中,并没有创建过事务,这个resources取出的是一个空的对象

doGetTransaction执行完之后,就是第二个步骤,isExistingTransaction,见名知意,判断是否已经存在了事务

img

根据我们刚刚的描述,是还没创建事务的,所以此时是false,于是便开始了创建事务,创建事务分为几个核心的步骤,其中最核心的莫过于三个

  1. new Connection和new ConnectionHolder(newCon)
  2. setAutoCommit(false) 开启事务
  3. TransactionSynchronizationManager#bindResource

第一步创建了连接,第三步则是将datasource对象作为key,ConnectionHolder作为value,绑定到线程中

image.png

这个方法执行完之后,此时已经创建了事务,并且将连接绑定到了线程中,再来回顾整个过程

img

创建完毕的事务对象,其实也就是连接对象,会封装到TransactionStatus对象中,这个对象在接下来的prepareTransactionInfo中被封装成TransactionInfo并同样绑定到线程中

img

getTransaction和prepareTransactionInfo都执行完了之后,createTransactionIfNecessary也就执行完了,并且最终获得TransactionInfo对象,层层关系间接持有了连接对象

img

这里看起来好像很复杂,但是实际上只有记住TransactionInfo已经持有了连接对象即可。prepareTransactionInfo执行完了之后,就会触发实际方法的调用,也就是业务代码的执行

再回顾下外层的逻辑

@Service
public class TestServiceImpl implements TestService {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private TestService2 testService2;

    @Override
    @Transactional
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')");
        testService2.test();
        throw new RuntimeException();
    }
}

当外层执行完毕写入数据之后,触发的便是内层的事务调用,同样,内层会再从刚刚那个入口出发,,也就是invokeWithinTransaction

img

又是熟悉的方法名,第一个便是createTransactionIfNecessary

img

同样,继续是getTransaction

img

接着继续是doGetTransaction,再来回顾下代码

image.png

在外层的代码逻辑中,我们知道这里是在以Datasource对象为key,取出连接对象时,因为是第一次取出,导致了对象为空。那么此时内层再取,是否还是为空呢?

答案是否定的。此时取出的便是第一次构建的连接对象。那么这里又有一个新的问题,取了就一定会用到吗?我们接着看getTransaction的isExistingTransaction。因为此时我们已经存在了一个事务,所以此时这个方法返回的是true

img

因为是true,所以此时和外层的逻辑便有不同,进入的是handleExistingTransaction,处理已经存在的事务。在这里,便会用到传播特性了。

img

因为此时内层的传播特性是PROPAGATION_REQUIRES_NEW,我们复习下,根据特性,这里其实会开启一个新的事务。

**那么问题就来了,开了新的事务,旧的事务去哪里呢??**这里就引出这里的步骤1,suspend方法

img

suspend又分为两个部分,一个是doUnbindResource,另一个是构建SuspendedResourcesHolder,根据我们对getResource和bindResource的理解,可以很容易知道doUnbindResource,就是将ConnectionHolder从先线程中解绑

image.png

解绑之后,将删除的返回值,也就是ConnectionHolder对象,暂存到SuspendedResourcesHolder。

那么,这个SuspendedResourcesHolder又是何方神圣呢?他其实也可以看做TransactionInfo的一个属性,专门用于存储被挂起的对象

img

这里就回答了上面那个问题,上一个事务的相关资源,被暂存到当前事务的SuspendedResourcesHolder属性中,挂起之后,便是重新开启一个新的事务startTransaction,并执行业务逻辑,这里的逻辑我们不再赘述,跟外层一样。

按照案例的代码,这里内层会正常执行,并且提交事务。

@Service
public class TestServiceImpl2 implements TestService2 {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhaoqijun')");
        return "2";
    }

}

所以就会来到invokeWithinTransaction的第三个部分,也就是commitTransactionAfterReturning

img

commitTransactionAfterReturning,显而易见,无非就是在返回执行完之后提交事务,这里的逻辑分为两个部分

img

第一个部分就是真正提交事务,重要的是第二个部分,这里再次完成了事务的交接,当前线程中的事务从内层的事务切换为外层的事务

image.png

具体的逻辑便是将事务对象中的SuspendedResourcesHolder中的ConnectionHolder取出,并且重新绑定到线程中。

将事务恢复到外层事务后,此时就应该继续往下执行外层事务了。我们再看下外层事务的代码

@Service
public class TestServiceImpl implements TestService {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private TestService2 testService2;

    @Override
    @Transactional
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')");
        testService2.test();
        throw new RuntimeException();
    }
}

内层的方法执行完毕之后,继续执行的话,便是抛出一个异常,这个异常会导致事务的回滚,因此会触发新逻辑,也就是invokeWithinTransaction中的另一个部分completeTransactionAfterThrowing

img

completeTransactionAfterThrowing分为两个部分

  1. doRollback
  2. doResume

步骤一便是回滚业务逻辑了,注意,此时内层和外层是不同的事务,而且内层事务已经提交,所以回滚的是外层的事务。至于步骤二,因为这里已经是最外层的事务了,没有什么什么可以doResume的,因此忽略。

到这里,案例一的源码分析就已经看完了。总结下流程

img

接下来是案例二,案例二相比案例一,唯一的区别是,内层的传播特性从REQUIRES_NEW变成了NESTED,我们回顾案例一的整个逻辑,传播特性的作用发生在外层事务切到内层事务的那一步,也就是

内层调用的isExistingTransaction。

所以在这个案例中,我们跳过外层事务,直接到内层事务的分析。入口同样是invokeWithinTransaction

img

第一步同样是createTransactionIfNecessary,在这里,首先从ThreadLocal中获取外层的事务,之后进入isExistingTransaction中,显而易见,此时isExistingTransaction的结果是true

img

因此继续处理这个已存在的外层事务,同时,此时的传播特性为NESTED

我们回顾下案例一,当内层的事务传播特性为REQUIRES_NEW的时候,内层的逻辑是挂起旧事务,创建一个新的事务。而此时为Nested,我们知道,当内层为Nested的时候,如果外层事务异常,内层必然回滚,这就是和 REQUIRES_NEW 的区别。那么Spring中是如何实现的呢?

img

核心的一步是,createAndHoldSavepoint,利用外层的事务,设置了一个savePoint,所以这里其实是利用了Mysql的savePoint。

image.png

所以在Nested的逻辑中,其实并没有创建新的事务,而是在外层的事务基础上,设置了一个savePoint

设置完savePoint之后,便开始下一步,执行对应的业务逻辑,执行结束后,跟案例一一样,来到了关键一步,就是提交事务commitTransactionAfterReturning

img

与案例一直接提交事务不同的是,这里判断了当前事务是否有savePoint,显而易见,这里结果为true

img

当有savePoint的时候,这里首先调用了releaseHeldSavepoint释放掉当前事务中的savePoint

image.png

然后继续外层事务,但是此时内外层是同一个事务,因此这个逻辑可以忽略。到这里,案例二的内层事务就执行完了,又回到了外层继续执行

@Service
public class TestServiceImpl implements TestService {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private TestService2 testService2;

    @Override
    @Transactional
    public String test() {
        jdbcTemplate.update("INSERT INTO test1(name) values ('zhoujielun')");
        testService2.test();
        throw new RuntimeException();
    }
}

外层抛出异常,之后触发回滚,这里的逻辑和案例一是一模一样的,不再赘述。但是需要注意的是,因为在案例二中,内外层是同一个事务,所以此时内层的逻辑也被回滚了,这点与案例一不同。

总结一下,案例二的整体流程

img

回顾整个案例,Spring事务整个流程中核心的内容有两个部分

  1. 事务的构建和线程绑定
  2. 内外层事务的交接,事务的挂起和恢复

这里也留给大家三个思考题,有兴趣的可以自行对源码做分析

  1. 多数据源情况下的事务切换是怎么做的
  2. 内层事务如果是异步的又会有哪些不同
  3. Nested模式如果是内层报错外层成功执行结果会怎么样呢