事务提交之后再执行某些操作 → 你有哪些实现方式?

开心一刻

昨晚,小妹跟我妈聊天

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

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

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

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

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

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

前情回顾

记一次线上问题,拿到的不是最新数据 → 偶尔的热情真的难顶呀!

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

那你到底是借还是不借?

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

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

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

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

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

事务拎出来

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

犯病拎

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

调整成如下

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

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

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

大家看仔细了,update上的@Transactional(rollbackFor = Exception.class)被拿掉了,不是漏写了!

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

是什么情况?

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

回到刚拎出来的情况,updateupdateUser在同一个类中,非事务方法update调用了事务方法updateUser,事务会怎么样?

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

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

你懵的同时,你老大也懵

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

巧了,正好我有答案:记一次线上问题 → 菜鸟杀手,事务为什么没生效

别扭拎

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

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

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

我们再看updateUser

调用链中有事务相关内容

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

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

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

要不将就着这么用?

常规拎

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

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

怎么这么多问题,非要去触及我的知识盲区?

那我就说几点:

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

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

3、导致一些莫名其妙的问题,还非常难以排查,大家可以搜索一下,关键字类似: 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

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

概况

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

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

从而在事务的不同阶段(如提交前、提交后、回滚后等)执行特定的操作(如发送消息)
TransactionSynchronizationManager 提供了很多静态方法, registerSynchronization 就是其中之一(其他的大家自行去学习)

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

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

底层原理

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

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

怎么揪?

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

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

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

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

java 复制代码
/**
 * 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

java 复制代码
/**
 * 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 使用有限制条件

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

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

相关推荐
fanruitian10 分钟前
Springboot aop面向切面编程
java·spring boot·spring
中国lanwp1 小时前
Spring Boot 中使用 Lombok 进行依赖注入的示例
java·spring boot·后端
胡萝卜的兔1 小时前
golang -gorm 增删改查操作,事务操作
开发语言·后端·golang
永日456703 小时前
学习日记-spring-day45-7.10
java·学习·spring
掘金码甲哥3 小时前
Golang 文本模板,你指定没用过!
后端
lwb_01184 小时前
【springcloud】快速搭建一套分布式服务springcloudalibaba(四)
后端·spring·spring cloud
张先shen6 小时前
Spring Boot集成Redis:从配置到实战的完整指南
spring boot·redis·后端
Dolphin_海豚6 小时前
一文理清 node.js 模块查找策略
javascript·后端·前端工程化
EyeDropLyq7 小时前
线上事故处理记录
后端·架构
javadaydayup9 小时前
别再逐个注入了!@Autowired 批量获取接口实现类的核心逻辑拆解
spring