开心一刻

昨晚,小妹跟我妈聊天

小妹:妈,跟你商量个事,我想换车,资助我点呀

妈:哎呀,你那分扣的攒一堆都够考清华的,还换车资助点,有车开就不错了

小妹:你要是这么逼我,别说哪天我去学人家傍大款啊

妈:哎呀妈,你脸上那褶子比你人生规划都清晰,咋地,大款缺地图呀,找你?

小妹:让我回到我18岁,大个、水灵、白,你再看看

妈:你18长的像黑鱼棒似的,还水灵白,消防栓水灵,也没见谁娶它呀,女人呐,你得有内涵

前情回顾


记一次线上问题 → 偶尔的热情真的难顶呀!

我们知道了女神偶尔的消息可能是借钱

那你到底是借还是不借?

不好意思,貌似抓错重点了

重点应该是:
把消息发送从事务中拎出来就好了,也就是等事务提交后,再发消息

什么,没看
记一次线上问题 → 偶尔的热情真的难顶呀!
,不知道重点,那还不赶紧去看?

我光提了重点,但是没给你们具体实现,就问你们气不气?

本着认真负责的态度,我还是提供几种实现,谁让我太宠你们了

事务拎出来

说起来很简单,做起来其实也很简单

犯病拎

为了更接近真实案例,我把

调整一下

User更新

插入操作日志
在一个事务中,
发消息
需要拎出去

拎出去还不简单,看我表演

相信大家都能看懂如上代码,上游调用
update
的地方也不用改,简直完美!

大家看仔细了,
update
上的
@Transactional(rollbackFor = Exception.
class
)

被拿掉了,不是漏写了!

如果
update
上继续保留
@Transactional(rollbackFor = Exception.
class
)

是什么情况?

那不是和没拎出来一样了吗?特么的还多写了几行代码!

回到刚拎出来的情况,
update

updateUser
在同一个类中,非事务方法
update
调用了事务方法
updateUser
,事务会怎么样?

如果你还没反应过来,八股文需要再背一背了:
在同一个类中,一个非事务方法调用另一个事务方法,事务不会生效

恭喜你,解决一个
bug
的同时,成功引入了另一个
bug

你懵的同时,你老大也懵

你们肯定会问:非事务方法
update
调用事务方法
updateUser
,事务为什么会失效了?

巧了,正好我有答案:
记一次线上问题 → 事务去哪了

别扭拎

同一个类中,非事务方法调用事务方法,事务不生效的解决方案中,是不是有这样一种解决方案:
自己注册自己

我们
debug
一下,看下堆栈情况

我们先看
update

调用链中没有事务相关内容

我们再看
updateUser

调用链中有事务相关内容

从结果来看,确实能够满足要求,上游调用
update
的地方也不用调整,并且还自给自足,感觉是个好方案呀


自己注册自己
这种情况,你们见得多吗,甚至见过吗

反正我看着好别扭,不知道你们有这种感觉没有?

要不将就着这么用?

常规拎

自己注册自己
是非常不推荐的!

为什么不推荐? 来来来,把脸伸过来

怎么这么多问题,非要把我榨干?

那我就说几点

1、违反了单一职责原则,一个类应该只负责一件事情,如果它开始依赖自己,那么它的职责就不够清晰,这可能会导致代码难以维护和扩展

2、循环依赖,自己依赖自己就是最简单版的循环依赖,虽说
Spring
能解决部分循环依赖,但
Spring
是不推荐循环依赖写法的

3、导致一些莫名其妙的问题,还非常难以排查,大家可以
Google
一下,关键字类似:
Spring 自己注入自己 有什么问题

推荐的做法是新建一个
UserManager
,类似如下

此时,上游调用的地方也需要调整,改调用
com.qsl.manager.UserManager#update
,如下所示:

同样
debug
下,来看看堆栈信息

com.qsl.manager.UserManager#update
调用栈情况如下

非常简单,没有任何的代理

我们再看下
com.qsl.service.impl.UserServiceImpl#updateUser

此时,调用链中是有事务相关内容的

是不是很完美的将消息发送从事务中抽出来了?

这确实也是我们最常用的方式,没有之一!

惊喜拎

既不想新增
UserManager
,又想把消息发送从事务中抽离出来,还要保证事务生效,并且不能用
自己注册自己
,有什么办法吗

好处全都要,坏处往外撂,求求你,做个人吧


但是,注意转折来了!

最近我还真学了一个新知识:
TransactionSynchronizationManager
,发现它完美契合上述的
既要、又要、还要、并且要

我们先回到最初的版本

接下来看我表演,稍微调整下代码

什么,调整了哪些,看的不够直观?

我真是服了你们这群老六,那我就再爱你们一次,让你们看的更直观,直接
beyond compare

就调整这么一点,上游调用
update
的地方也不用调整,你们的
既要、又要、还要、并且要
就满足了!

是不是很简单?

为了严谨,我们来验证一下

如何验证了?

最简单的办法就是在发送消息的地方打个断点,如下所示


debug
执行到此的时候,消息是未发送的,这个没问题吧?

那么我们只需要验证:此时事务是否已经提交

问题又来了,如何验证事务已经提交了呢?

很简单,我们直接去数据库查对应的记录,是不是修改之后的数据,如果是,那就说明事务已经提交,否则说明事务没提交,能理解吧?

我们以修改 张三 的密码为例,
bebug
未开始,此时 张三 的密码是
zhangsan1

我们把 张三 的密码改成
zhangsan2

开始
bebug

此时,消息还未发送,我们去数据库查下 张三 的密码

此时 张三 的密码已经是
zhangsan2
了,是修改之后的数据,说明了什么?

说明事务已经提交了,而此时消息还未发送!
是不是很优雅的实现了最初的重点:
把消息发送从事务中拎出来就好了,也就是等事务提交后,再发消息

TransactionSynchronizationManager

从字面意思来看,就是一个事务同步管理器

概况

TransactionSynchronizationManager

Spring
框架中提供的一个工具类,主要用于
管理事务的同步操作

通过
TransactionSynchronizationManager
,开发者可以自定义实现
TransactionSynchronization
接口或继承
TransactionSynchronizationAdapter

从而在事务的不同阶段(如提交前、提交后、回滚后等)执行特定的操作(如发送消息)

TransactionSynchronizationManager
提供了很多静态方法,
registerSynchronization
就是其中之一(其他的大家自行去学习)

入参类型是
TransactionSynchronization
,该接口定义了几个事务同步方法(命名很好,见名知意)

分别代表着在事务的不同阶段,会被执行的操作,比如
afterCommit
会在事务提交后执行

底层原理

为什么事务提交后一定会执行
org.springframework.transaction.support.TransactionSynchronization#afterCommit

幕后一定有操盘手,我们来揪一揪它

怎么揪?

正所谓:
源码之下无密码
,我们直捣黄龙干源码

问题又来了,
Spring
源码那么多,我们怎么知道哪一部分跟
TransactionSynchronization
有关?

很简单,去
bebug
的堆栈中找,很容易就能找到切入点

切入点是不是很明显了:
org.springframework.transaction.support.AbstractPlatformTransactionManager#commit


/*** This implementation of commit handles participating in existing
* transactions and programmatic rollback requests.
* Delegates to {
@codeisRollbackOnly}, {@codedoCommit}
* and {
@coderollback}.
*
@seeorg.springframework.transaction.TransactionStatus#isRollbackOnly()
*
@see#doCommit
*
@see#rollback*/@Overridepublic final void commit(TransactionStatus status) throwsTransactionException {if(status.isCompleted()) {throw newIllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus
=(DefaultTransactionStatus) status;if(defStatus.isLocalRollbackOnly()) {if(defStatus.isDebug()) {
logger.debug(
"Transactional code has requested rollback");
}
processRollback(defStatus,
false);return;
}
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;
}

processCommit(defStatus);
}

View Code

通过
commit
的源码,或者上图的调用链,我们会继续来到
org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit


/*** Process an actual commit.
* Rollback-only flags have already been checked and applied.
*
@paramstatus object representing the transaction
*
@throwsTransactionException in case of commit failure*/ private void processCommit(DefaultTransactionStatus status) throwsTransactionException {try{boolean beforeCompletionInvoked = false;try{boolean unexpectedRollback = false;
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked
= true;if(status.hasSavepoint()) {if(status.isDebug()) {
logger.debug(
"Releasing transaction savepoint");
}
unexpectedRollback
=status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
else if(status.isNewTransaction()) {if(status.isDebug()) {
logger.debug(
"Initiating transaction commit");
}
unexpectedRollback
=status.isGlobalRollbackOnly();
doCommit(status);
}
else if(isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback
=status.isGlobalRollbackOnly();
}
//Throw UnexpectedRollbackException if we have a global rollback-only//marker but still didn't get a corresponding exception from commit. if(unexpectedRollback) {throw newUnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch(UnexpectedRollbackException ex) {//can only be caused by doCommit triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);throwex;
}
catch(TransactionException ex) {//can only be caused by doCommit if(isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else{
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throwex;
}
catch (RuntimeException |Error ex) {if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throwex;
}
//Trigger afterCommit callbacks, with an exception thrown there//propagated to callers but the transaction still considered as committed. try{
triggerAfterCommit(status);
}
finally{
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}

}
finally{
cleanupAfterCompletion(status);
}
}

View Code

大家仔细看这个方法,在
doCommit(status)
之前有
triggerBeforeCommit(status)

triggerBeforeCompletion(status)

doCommit(status)
之后有
triggerAfterCommit(status)

triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED)

这几个方法的作用很明显了吧(
trigger
是触发的意思)

接下来我们跟哪个方法?

很明显,我们要跟
triggerAfterCommit(status)
,因为我们要找的是
afterCommit
的操盘手

内容很简单,下一步跟的对象也很明确

这里要分两步说明下

1、
TransactionSynchronizationManager.getSynchronizations()

先获取所有的事务同步器,然后进行排序

排序先撇开,我们先看看获取到了哪些事务同步器

第一个不眼熟,我们先不管

第二个眼不眼熟?是不是就是
com.qsl.service.impl.UserServiceImpl#update
中的匿名内部类?(如果想看的更明显,就不要用匿名内部类)

是不是就对应上了:先注册,再获取,最后被调用

被调用就是下面的第 2 步

2、
invokeAfterCommit

逻辑很简单,遍历所有事务同步器,逐个调用事务同步器的
afterCommit
方法

我们案例中的
发消息
就是在此处被执行了

至此,相信大家都没疑惑了吧

总结

1、关于
Spring
循环依赖,大家可以翻阅下我之前的博客

Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的?

三探循环依赖 → 记一次线上偶现的循环依赖问题

四探循环依赖 → 当循环依赖遇上 BeanPostProcessor,爱情可能就产生了!

总之一句话:一定要杜绝循环依赖!

2、事务提交之后再执行某些操作的实现方式

事务失效的方式,大家一定要警惕,这坑很容易掉进去

自己注册自己的方式,直接杜绝,就当没有这种方式

Manager
方式很常规,可以使用

TransactionSynchronizationManager
方式很优雅,推荐使用

看了这篇博客后,该用哪种方式,大家心里有数了吧

3、TransactionSynchronizationManager 使用有限制条件

具体看其注释说明,就当给你们留的家庭作业了

一定要去看,不然使用出了问题可别怪我没提醒你们

标签: none

添加新评论