事务提交之后再执行某些操作 → 引发对 TransactionSynchronizationManager 的探究
开心一刻
昨晚,小妹跟我妈聊天
小妹:妈,跟你商量个事,我想换车,资助我点呀
妈:哎呀,你那分扣的攒一堆都够考清华的,还换车资助点,有车开就不错了
小妹:你要是这么逼我,别说哪天我去学人家傍大款啊
妈:哎呀妈,你脸上那褶子比你人生规划都清晰,咋地,大款缺地图呀,找你?
小妹:让我回到我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 {@code isRollbackOnly}, {@code doCommit}
* and {@code rollback}.
* @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
* @see #doCommit
* @see #rollback
*/
@Override
public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"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);
}
通过 commit 的源码,或者上图的调用链,我们会继续来到 org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit
/**
* Process an actual commit.
* Rollback-only flags have already been checked and applied.
* @param status object representing the transaction
* @throws TransactionException in case of commit failure
*/
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
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 new UnexpectedRollbackException(
"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);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException | Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
} // 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);
}
}
大家仔细看这个方法,在 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 使用有限制条件
具体看其注释说明,就当给你们留的家庭作业了
一定要去看,不然使用出了问题可别怪我没提醒你们
事务提交之后再执行某些操作 → 引发对 TransactionSynchronizationManager 的探究的更多相关文章
- ajax请求执行完成后再执行其他操作(jQuery.page.js插件使用为例)
就我们做知,ajax强大之处在于它的异步请求,但是有时候我们需要ajax执行彻底完成之后再执行其他函数或操作 这个时候往往我们用到ajax的回调函数,但是假如你不想或者不能把接下来的操作写在回调函数中 ...
- iOS AFNetWorking中block执行完后再执行其它操作
需求:同时进行两次网络请求,网络请求是异步的,在网络请求成功后进行其它的操作.两个网络请求是这样,一个网络请求中block执行完之后,再进行其它操作,也是一样的原理,只是这时候不需要线程组了,只需要信 ...
- 如何等待ajax完成再执行相应操作
ajax广泛应用于异步请求,对于大多数业务来说,这是十分方便的,但对于一些特殊的业务,ajax的异步性会起到相反的作用. 例如在ajax请求成功后,后续的操作需要依赖ajax执行成功后的相应操作. / ...
- ExtJS 等待两个/多个store加载完再执行操作的方法
ExtJS是一种主要用于创建前端用户界面,是一个基本与后台技术无关的前端ajax框架. Extjs加载Store是异步加载的,这有很多好处.但是当我们要在两个或多个不同的store加载完再执行一些操作 ...
- Transactional事务提交后触发异步方法
一.问题复现 1.场景 2个service方法, 方法A中调用方法B. 方法A 是核心业务方法,涉及多张表数据变更,为了保持数据一致,用spring事务注解:@Transactional(rollba ...
- Mysql执行Update操作时会锁住表
update tableA a,(select a.netbar_id,sum(a.reward_amt) reward_amt from tableB a group by a.netbar_id) ...
- 如何同时完成多个ajax之后再执行某个方法 ? 使用$.when().done();
jQuery中的$.when()方法比较复杂,这里不作全面讲解,只写一个同时完成多个ajax请求后执行操作的方法. 有时候我们需要等待多个ajax执行完以后,再执行某个操作. 写法如下: $.when ...
- CentOS 7在执行yum操作时 报错
CentOS 7在执行yum操作时, 报错:Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=6&arch ...
- spring执行事务提交后进行一些逻辑操作
在使用spring事务时,我们通常会把事务内的所有操作当成是一个原子操作.也就是当整个事务内的所有代码都执行完成后, 才会将所有的数据落实到数据库中.这样做有时也会给我们造成麻烦.比如以下场景: 根据 ...
- 【Spring】20、使用TransactionSynchronizationManager在spring事务提交之后进行一些操作。
本文内容 如何在spring事务提交之后进行一些操作,这些操作必须得在该事务成功提交后才执行,回滚则不执行. 要点 如何在spring事务提交之后操作 如何在spring事务回滚之后操作 实现方案 使 ...
随机推荐
- Axure 标记元件
快照:可以用来表示控件的截图功能 箭头:有了连线,基本很少用它 便签:相关于便利贴,写些说明.备注, 标记:标记好数字,对应数字的标记做解释说明
- Kubernetes(K8S) yaml 介绍
使用空格做为缩进 缩进的空格数目不重要, 只要相同层级的元素左侧对齐即可 低版本缩进时不允许使用 Tab 键, 只允许使用空格 使用#标识注释, 从这个字符一直到行尾, 都会被解释器忽略 --- 使用 ...
- Hadoop面试题总结(二)——HDFS
1. HDFS 中的 block 默认保存几份? 默认保存3份 2.HDFS 默认 BlockSize 是多大? 默认64MB 3.负责HDFS数据存储的是哪一部分? DataNode负责数据存储 4 ...
- Leaflet 地图偏移 地图纠偏
(地图瓦片纠偏最好的方法在这:https://www.cnblogs.com/s0611163/p/15606460.html) 地图区域是一个市,偏移量可以近似认为是固定不变的,通过修改Leafle ...
- 核心工具之 ideavim
对于个人的核心工具集的选择,因为编程中熟练使用主力IDE能够大大提高效率,所以IDE是一个必选项.而IDE与VIM结合的插件,对于VIM使用者,常常是一个必选项. 在Jetbrains系列工具中,插件 ...
- C++20 | std::span 陣列、容器的代理人
在 C++ 裡頭有相當多「容器」.從原生的陣列,到標準庫 STL 的 vector, array, list, queue, map, set, -.有時候我們只是想以檢視的角度去看一個容器,或是其中 ...
- Codeforce:4C. Registration system (映射)
A new e-mail service "Berlandesk" is going to be opened in Berland in the near future. The ...
- 【每日一题】21.边的染色 (DFS连通图 + 思维)
补题链接:Here 思维不够,看到这种陌生的题目无从下手. 这题应该做过一次的人会觉得它其实并不难. 主要思想:把边权->点权. 这样做的好处是,无论你怎么分配点权,在环内的异或值一定为 \(0 ...
- Codeforces Round #700 (Div. 2) A ~ D1个人题解
Codeforces Round #700 (Div. 2) 比赛链接: Click Here 1480A. Yet Another String Game 因为Alice是要追求小,Bob追求大值, ...
- Centos7 kubeadm安装k8s
安装环境准备 关闭防火墙 systemctl stop firewalld systemctl disable firewalld 关闭selinux sed -i 's/enforcing/disa ...