版本基于Spring5.3.8
基础知识介绍
在正式介绍SpringTx的源码之前,先来回顾下基础知识
Spring事务传播机制类型
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方法了
可以从invoke方法中找到核心的逻辑也就是invokeWithinTransaction,这个便是SpringTx的具体实现入口了。
源码分析我们先从案例一入手,首先代码进入的是外层的事务。从入口开始分析,invokeWithinTransaction分为几个核心的部分,分别如下
注意这里第三步是completeTransactionAfterThrowing,因为外层事务是抛出异常的。这几个步骤首先看第一个createTransactionIfNecessary,他的作用是获得一个
TransactionInfo,这个对象是贯穿整个Spring事务的核心对象,请务必记住这个名字,那么这个对象中有什么核心属性呢?注意这里并没有列出全部的属性
这里有两个属性,一个是TransactionStatus对象,一个是oldTransactionInfo,具体是做什么用呢,我们暂且按下不表。先看createTransactionIfNecessary中的逻辑便知。
首先第一个步骤是getTransaction,见名知意,这是获得事务对象的方法,那么从何处获得呢?分为两个步骤
首先是doGetTransaction
doGetTransaction其中核心的点是doGetResource方法,在这个方法中的核心逻辑是:调用resources的get方法获得一个映射,再取出这个映射中的value
org.springframework.transaction.support.TransactionSynchronizationManager#doGetResource
那么这里会衍生出来几个核心问题
- resources是什么?
- 取出的这个map,key和value又分别是什么
查看源码,可以看到resources是一个Threadlocal,存储的对象是一个Map,那么这个map又是什么呢
我们如果自行Debug源码,可以看到
- key是Datasource对象
- value是一个ConnectionHolder对象
ConnectionHolder对象中的核心参数如下
因此,总结一下,这个resources其实是以线程为维度,存储一个数据源对象和链接对象的映射。那么是否此时的调用doGetResource的结果就是这个ConnectionHolder呢?回到我们的案例中,此时是外层第一次调用,因此答案是否定的,因为此时线程中,并没有创建过事务,这个resources取出的是一个空的对象。
doGetTransaction执行完之后,就是第二个步骤,isExistingTransaction,见名知意,判断是否已经存在了事务
根据我们刚刚的描述,是还没创建事务的,所以此时是false,于是便开始了创建事务,创建事务分为几个核心的步骤,其中最核心的莫过于三个
- new Connection和new ConnectionHolder(newCon)
- setAutoCommit(false) 开启事务
- TransactionSynchronizationManager#bindResource
第一步创建了连接,第三步则是将datasource对象作为key,ConnectionHolder作为value,绑定到线程中
这个方法执行完之后,此时已经创建了事务,并且将连接绑定到了线程中,再来回顾整个过程
创建完毕的事务对象,其实也就是连接对象,会封装到TransactionStatus对象中,这个对象在接下来的prepareTransactionInfo中被封装成TransactionInfo并同样绑定到线程中
getTransaction和prepareTransactionInfo都执行完了之后,createTransactionIfNecessary也就执行完了,并且最终获得TransactionInfo对象,层层关系间接持有了连接对象
这里看起来好像很复杂,但是实际上只有记住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
又是熟悉的方法名,第一个便是createTransactionIfNecessary
同样,继续是getTransaction
接着继续是doGetTransaction,再来回顾下代码
在外层的代码逻辑中,我们知道这里是在以Datasource对象为key,取出连接对象时,因为是第一次取出,导致了对象为空。那么此时内层再取,是否还是为空呢?
答案是否定的。此时取出的便是第一次构建的连接对象。那么这里又有一个新的问题,取了就一定会用到吗?我们接着看getTransaction的isExistingTransaction。因为此时我们已经存在了一个事务,所以此时这个方法返回的是true
因为是true,所以此时和外层的逻辑便有不同,进入的是handleExistingTransaction,处理已经存在的事务。在这里,便会用到传播特性了。
因为此时内层的传播特性是PROPAGATION_REQUIRES_NEW,我们复习下,根据特性,这里其实会开启一个新的事务。
**那么问题就来了,开了新的事务,旧的事务去哪里呢??**这里就引出这里的步骤1,suspend方法
suspend又分为两个部分,一个是doUnbindResource,另一个是构建SuspendedResourcesHolder,根据我们对getResource和bindResource的理解,可以很容易知道doUnbindResource,就是将ConnectionHolder从先线程中解绑
解绑之后,将删除的返回值,也就是ConnectionHolder对象,暂存到SuspendedResourcesHolder。
那么,这个SuspendedResourcesHolder又是何方神圣呢?他其实也可以看做TransactionInfo的一个属性,专门用于存储被挂起的对象
这里就回答了上面那个问题,上一个事务的相关资源,被暂存到当前事务的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
commitTransactionAfterReturning,显而易见,无非就是在返回执行完之后提交事务,这里的逻辑分为两个部分
第一个部分就是真正提交事务,重要的是第二个部分,这里再次完成了事务的交接,当前线程中的事务从内层的事务切换为外层的事务
具体的逻辑便是将事务对象中的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
completeTransactionAfterThrowing分为两个部分
- doRollback
- doResume
步骤一便是回滚业务逻辑了,注意,此时内层和外层是不同的事务,而且内层事务已经提交,所以回滚的是外层的事务。至于步骤二,因为这里已经是最外层的事务了,没有什么什么可以doResume的,因此忽略。
到这里,案例一的源码分析就已经看完了。总结下流程
接下来是案例二,案例二相比案例一,唯一的区别是,内层的传播特性从REQUIRES_NEW变成了NESTED,我们回顾案例一的整个逻辑,传播特性的作用发生在外层事务切到内层事务的那一步,也就是
内层调用的isExistingTransaction。
所以在这个案例中,我们跳过外层事务,直接到内层事务的分析。入口同样是invokeWithinTransaction
第一步同样是createTransactionIfNecessary,在这里,首先从ThreadLocal中获取外层的事务,之后进入isExistingTransaction中,显而易见,此时isExistingTransaction的结果是true
因此继续处理这个已存在的外层事务,同时,此时的传播特性为NESTED
我们回顾下案例一,当内层的事务传播特性为REQUIRES_NEW的时候,内层的逻辑是挂起旧事务,创建一个新的事务。而此时为Nested,我们知道,当内层为Nested的时候,如果外层事务异常,内层必然回滚,这就是和 REQUIRES_NEW
的区别。那么Spring中是如何实现的呢?
核心的一步是,createAndHoldSavepoint,利用外层的事务,设置了一个savePoint,所以这里其实是利用了Mysql的savePoint。
所以在Nested的逻辑中,其实并没有创建新的事务,而是在外层的事务基础上,设置了一个savePoint。
设置完savePoint之后,便开始下一步,执行对应的业务逻辑,执行结束后,跟案例一一样,来到了关键一步,就是提交事务commitTransactionAfterReturning
与案例一直接提交事务不同的是,这里判断了当前事务是否有savePoint,显而易见,这里结果为true
当有savePoint的时候,这里首先调用了releaseHeldSavepoint释放掉当前事务中的savePoint
然后继续外层事务,但是此时内外层是同一个事务,因此这个逻辑可以忽略。到这里,案例二的内层事务就执行完了,又回到了外层继续执行
@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();
}
}
外层抛出异常,之后触发回滚,这里的逻辑和案例一是一模一样的,不再赘述。但是需要注意的是,因为在案例二中,内外层是同一个事务,所以此时内层的逻辑也被回滚了,这点与案例一不同。
总结一下,案例二的整体流程
回顾整个案例,Spring事务整个流程中核心的内容有两个部分
- 事务的构建和线程绑定
- 内外层事务的交接,事务的挂起和恢复
这里也留给大家三个思考题,有兴趣的可以自行对源码做分析
- 多数据源情况下的事务切换是怎么做的
- 内层事务如果是异步的又会有哪些不同
- Nested模式如果是内层报错外层成功执行结果会怎么样呢