这里感谢 CSDN 的原博客: https://blog.csdn.net/qq_41737716/article/details/84864904
1、前情概要
上篇文章介绍了TransactionInterceptor这个事务增强器到底干了什么,这篇文章将更仔细介绍一些事务的操作,例如挂起是什么,有什么用,解绑绑定又有什么用等等,然后会结合实例介绍各个场景下的传播特性,相信只要看到这里,前面两篇文章理解了之后,对Spring的事务运用的将会得心应手。
2、回顾
2.1、什么时候holder中的Active为true?
只有在执行了doBegin方法时才会将holder设置为true。
2.2、回滚的几个关键点!
status.hasSavepoint()
如果status中有savePoint,只回滚到savePoint!、status.isNewTransaction()
如果status是一个新事务,才会真正去回滚!status.hasTransaction()
如果status有事务,将会对staus中的事务标记!
2.3、提交的几个关键点!
status.hasSavepoint()
如果status有savePoint,说明此时的事务是嵌套事务NESTED,这个事务外面还有事务,这里不提交事务,只是释放保存点。这里也可以看出来NESTED的传播行为了,外层事务成功提交它才会真正提交,外层事务失败它也将回滚。status.isNewTransaction()
如果是新的事务,才会提交!!
3、挂起与恢复
3.1、挂起
回顾一下上一篇文章,挂起的作用是什么。这里挂起主要干了三件事:
- 将
transaction
中的holder属性设置为空 - 解绑(会返回线程中的那个旧的holder出来,从而封装到
SuspendedResourcesHolder
对象中) - 将解绑得到的
SuspendedResourcesHolder
放入status中
为什么要这样呢?有下面几种原因: transaction
中的holder
为空了,将会导致doBegin方法会新创建一个连接:
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
}
这里截取了一段doBegin方法,只展示挂起之后执行doBegin会有什么不一样,首先会进入第一个if语句块,会从dataSource
中取得一个新连接,然后new一个holder
存放在transaction
中,并标记为新holder
。
然后,会执行绑定操作,将新holder绑定到线程变量Map中。
- 将线程变量中旧holder取出来,使线程变量中holder为空,如果后面没有doBegin操作,还有一个事务进来的话,就会进入创建事务的方法:
@Override
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
如果挂起了,就不会取到上一个事务的holder了。
3.2、恢复
回顾一下,恢复中做了哪些事情:
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources);
}
这里恢复只是把suspendedResources
重新绑定到线程中。
我们需要关注的地方是,什么时候会恢复呢?
在提交或是回滚时finally语句块始终会执行清理方法cleanupAfterCompletion判断status对象中是否有挂起的对象:
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
if (status.getSuspendedResources() != null) {
Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
}
}
也就是说,有挂起(除了挂起null的情况),就会有恢复!
那么恢复有什么作用呢?还是拿出同样的方法:
@Override
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
如果恢复了,下一个子事务进来时,创建事务的时候将会知道我上面有存在事务,不恢复的话我的子事务根本不知道上面有事务isExistingTransaction(transaction) 方法直接返回false,因为此时没恢复的话holder是为null的(执行完事务会判断如为新的holder,将holder解绑),直接按照不存在事务的情况去判断事务如何进行。
4、绑定与解绑
4.1、绑定
简要回顾绑定是什么意思,它将当前的holder作为value保存在当前线程变量Map中,key为dataSource。
那么绑定有什么用呢?其实上面也是有提到了已经,在doGetTransaction方法中,会获取线程变量中的holder,如果此时有绑定的话,就可以获取到上一个事务绑定的holder,如果没有绑定或是解绑了,就会获取一个null的holder,主要用于后面判断是否已经存在于事务中。
4.2、解绑
简要回顾解绑是什么意思,它将旧的holder从当前线程变量Map中移除,并且返回出去。
解绑又有什么用呢?上面也有提到,在doBegin方法中是可以获取一个新连接的,解绑其实伴随着挂起,挂起中包含了解绑,还有一个作用是把线程中holder变为空,那么下一个事务进入doGetTransaction会获取一个null的holder。其实说到这里有点废话啰嗦的感觉了,其实挂起恢复绑定解绑都几乎有很多相同的目的,其中都是互相伴随着执行的。
5、结合传播特性去理解挂起、恢复、绑定、解绑
5.1、解析几种需要挂起的传播特性的情况
5.1.1、(当前存在事务情况下):
- 传播特性为REQUIRES_NEW :此时会先挂起,然后去执行doBegin方法,在上面也有说到先挂起在doBegin的作用,此时会创建一个新连接,新holder,新holder有什么用呢?
如果是新holder,会在doBegin中做绑定操作,其次,在提交或是回滚时finally语句块始终会执行清理方法时判断新holder会进行解绑操作。
@Override
protected void doCleanupAfterCompletion(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
// Remove the connection holder from the thread, if exposed.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.unbindResource(obtainDataSource());
}
}
符合传播特性,所以这里REQUIRES_NEW这个传播特性是与原事务相隔的,用的连接都是新new出来的。
此时返回的status是这样的:
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
其中transaction中holder为新holder,连接都是新的。标记为新事务,在开头的回顾中提到,如果是新事务,提交时才能成功提交。并且在最后一个参数放入挂起的对象,之后将会恢复它。
5.1.1.1、存在事务情况下的REQUIRES_NEW总结:
会于前一个事务隔离,自己新开一个事务,与上一个事务无关,如果报错,上一个事务catch住异常,上一个事务是不会回滚的,而只要自己提交了之后,就算上一个事务报错,自己是不会回滚的(因为被标记为新事务,所以在提交阶段已经提交了)。
- 传播特性为NESTED :不挂起事务,并且返回的status对象如下:
DefaultTransactionStatus status =
prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
不同于其他的就是此传播特性会创建savePoint,有什么用呢?前面说到,如果是旧事务的话回滚是不会执行的,但先看看它的status,虽然标记为旧事务,但它还有savePoint,在开头回顾中,回滚的关键点提到了,如果有savePoint,会回滚到保存点去,提交的时候,会释放保存点,但是不提交!切记,这里就是NESTED与REQUIRES_NEW不同点之一了,NESTED只会在外层事务成功时才进行提交,实际提交点只是去释放保存点,外层事务失败,NESTED也将回滚,但如果是REQUIRES_NEW的话,不管外层事务是否成功,它都会提交不回滚。这就是savePoint的作用。
由于不挂起事务,可以看出来,此时transaction中的holder用的还是旧的,连接也是上一个事务的连接,可以看出来,这个传播特性会将原事务和自己当成一个事务来做。
5.1.1.2、存在事务情况下的NESTED总结:
与前一个事务不隔离,没有新开事务,用的也是老transaction,老的holder,同样也是老的connection,没有挂起的事务,所以也不会去执行恢复(也没有必要)。关键点在这个传播特性在存在事务情况下会创建savePoint,但不存在事务情况下是不会创建savePoint的(下面会提到)。在提交时不真正提交,只是释放了保存点而已,在回滚时会回滚到保存点位置,如果上层事务catch住异常的话,是不会影响上层事务的提交的。
- 传播特性为REQUIRED :是不会挂起的,并且status为:
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
使用旧事务,标记为旧事务,挂起对象为空。
5.1.1.3、存在事务情况下的REQUIRED总结:
与前一个事务不隔离,没有新开事务,用的也是老transaction,老的connection,但此时被标记成旧事务,所以,在提交阶段不会真正提交的,在外层事务提交阶段,才会把事务提交。
值得一提的是如果此时这里出现了异常,内层事务执行回滚时,由开头回顾提到,旧事务是不会去回滚的,但据我们经验所知,如果内层事务抛出异常,传播特性为REQUIRED 时,应该是同一个事务,内层事务不回滚,那外层事务总得回滚吧?那此时外层事务是怎么知道需要回滚的呢?
- 内层事务抛出异常,外层事务没有捕获,把异常跑到外层事务中,自然外层事务会回滚的
- 但如果外层事务捕捉了异常,是否就不回滚了?不是这样的,就算捕获外层事务照样回滚,并且额外抛出一个异常:
在内层事务回滚时,有这样一段代码
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
// 取出status中的transaction
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
if (status.isDebug()) {
logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() +
"] rollback-only");
}
// 对transaction设置一个标记
txObject.setRollbackOnly();
}
public void setRollbackOnly() {
// 将transaction中holder设置了一个标记
getConnectionHolder().setRollbackOnly();
}
那么这个标记设置了有什么用呢?我们知道,在内层事务中transaction对象中的holder对象其实就是外层事务transaction里的holder,holder是一个对象,指向同一个地址,在这里设置holder标记,外层事务transaction中的holder也是会被设置到的,有什么用呢?在外层事务提交的时候有这样一段代码:
@Override
public final void commit(TransactionStatus status) throws TransactionException {
// 略...
// !shouldCommitOnGlobalRollbackOnly()只有JTA与JPA事务管理器才会返回false
// defStatus.isGlobalRollbackOnly()这里判断status是否被标记了
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
// 如果内层事务抛异常,外层事务是会走到这个方法中的,而不是去提交
processRollback(defStatus, true);
return;
}
// 略...
}
@Override
public boolean isGlobalRollbackOnly() {
return ((this.transaction instanceof SmartTransactionObject) &&
((SmartTransactionObject) this.transaction).isRollbackOnly());
}
@Override
public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}
到这里已经可以大概知道了,在外层事务提交的时候是会去验证transaction中的holder里是否被标记rollback了,也就是上面提到的,内层事务回滚,将会标记holder,而holder是线程变量,在此传播特性中holder是同一个对象,外层事务将无法正常提交而进入processRollback方法进行回滚,并抛出异常:
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
// 此时这个值为true
boolean unexpectedRollback = unexpected;
try {
triggerBeforeCompletion(status);
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
status.rollbackToHeldSavepoint();
}
// 新事务,将进行回滚操作
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
// 回滚!
doRollback(status);
}
// 略...
// Raise UnexpectedRollbackException if we had a global rollback-only marker
// 上面提到unexpectedRollback此时为true,抛出一个异常
if (unexpectedRollback) {
// 这个就是上文说到的抛出的异常类型
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
}
finally {
cleanupAfterCompletion(status);
}
}
- 传播特性为NOT_SUPPORTED:是会挂起的,此时status为:
return prepareTransactionStatus(
definition, null, false, newSynchronization, debugEnabled, suspendedResources);
transaction为空,旧事务,挂起的对象存入status中。
此时如果这个事务中还有子事务的话,其实是会新开一个连接并且与上上个事务隔离的,因为这里是进行挂起操作的,所以它的子事务获取holder的时候是为null的,而为null的话会进入无事务情况判断,并且会进行doBegin方法new一个connection出来,所以这个传播特性可以保证在其子事务中是不会有外层事务的(相当于在这个传播特性中,即使有上层事务,在内层事务内的内内层事务还是相当于第一次执行事务一样,对于内内层事务而言它没有外层事务)。
5.1.1.4、存在事务情况下的NOT_SUPPORTED 总结:
与外层事务隔离了,在这种传播特性下,是不进行事务的,当提交时,因为是旧事务,所以不会commit,失败时也不会回滚rollback,如果其有子事务,会保证子事务没有外层事务。
5.1.2、(当前不存在事务的情况下):
每次创建一个TransactionInfo的时候都会去new一个transaction,然后去线程变量Map中拿holder,当此时线程变量的Map中holder为空时,就视为当前情况下不存在事务,所以此时transaction中holder = null。
不存在事务的情况下比较简单一点,这里将REQUIRED、REQUIRES_NEW、NESTED合并在一起讲。
- 当传播特性为REQUIRED、REQUIRES_NEW、NESTED中的一种:此时会讲null挂起,在前面也有说过了,这时把null挂起不是为了操作holder,而是为了讲当前事务信息从线程变量中取出,再将存放事务信息的当前线程变量里的信息设为空而已,而且这里传入的参数为null,不会执行挂起操作,只会设置事务信息而已。此时的status变量为:
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
此时的transaction中holder依然为null,标记为新事务,将刚刚取出的事务信息存放进最后一个参数。
接着就会执行doBegin方法了:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
// 此时会进入这个if语句块,因为此时的holder依然为null
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 从dataSource从取得一个新的connection
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// new一个新的holder放入新的连接,设置为新的holder
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
// 略...
prepareTransactionalConnection(con, definition);
// 将holder设置avtive = true
txObject.getConnectionHolder().setTransactionActive(true);
// Bind the connection holder to the thread.
// 绑定操作
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
}
所以,一切都是新的,新的事务,新的holder,新的连接,在当前不存在事务的时候一切都是新创建的。
5.1.2.1、存在事务情况下的REQUIRED、REQUIRES_NEW、NESTED总结:
在这里可以得出结论,这三种传播特性在当前不存在事务的情况下是没有区别的,此事务都为新创建的连接,在回滚和提交的时候都可以正常回滚或是提交,就像正常的事务操作那样。
其余的传播特性都比较简单,在上一篇文章中都有注释,相信注释已经能够看懂其余的传播特性是做什么用的,这里就不多做赘述了。
6、总结:
在本篇文章中首先回顾了一下回滚与提交的触发点,可以知道设置为新事务才可以进行回滚与提交的操作,而设置新事务的地方在下面也有提到。还介绍了挂起、恢复、绑定、解绑是什么,以及其作用,并且结合场景,对传播特性结合上面说的挂起、绑定等等操作进行了分析,分析了每一种场景下各个传播特性都会对事务产生怎样的影响,阅读完本篇文章,你可以了解到Spring中传播特性对事务的控制。
这里我说了很多源码与原理层面的东西,在下一篇文章中我将使用代码在实际场景中演示各个传播特性到底是怎么样的,各种异常场景都会重现,毕竟实践出真知,透过现象会更容易的看到本质。
(本来打算在这篇文章中就结束的,想不到写着写着又写了这么长出来。。只能放在下一章了)