关于spring嵌套事务,我发现网上好多热门文章持续性地以讹传讹

news2024/12/25 1:34:39

事情起因是,摸鱼的时候在某平台刷到一篇spring事务相关的博文,文章最后贴了一张图。里面关于嵌套事务的表述明显是错误的。

更奇怪的是,这张图有点印象。在必应搜索关键词PROPAGATION_NESTED出来的第一篇文章,里面就有这这部份内容,也是结尾部份完全一模一样。

更关键的是,人家原文是表格,这位倒好,估计是怕麻烦,直接给截成图片了。

而且这篇文章其实在评论区已经被人指出来这方面的问题了,谁也不能保证自己写的文章没有一点纰漏,但原作者并没有加以理会并修改错误。
这位转载作者呢也不加验证地直接拿走了。

这位转载作者可不是个小号,而是某年度的人气作者。
可能是有自己的公众号,得保持一定的更新频率?

好家伙,没经过验证,一部份错误的内容就这样被持续扩大传播了。

在必应搜索关键词PROPAGATION_NESTED出来文章,前两篇都是CSDN,都是一样的文章一样的错误。另外几篇文章也或多或少有些表述不清的地方。因此尝试来写一写这方面的东西。

顺便吐槽一下CSDN,我好多篇文章都被这上面的某些作者给扒过去,然后搜索一模一样的标题,权重比我还高,出来排第一位的反而是CSDN的盗版文章。

1.当我们在谈论嵌套事务的时候,嵌套的是什么?


当看到`嵌套事务`第一反应想到是这样式的:

但这更像PROPAGATION_REQUIRES_NEW啊,感兴趣可以去打断点执行一下。PROPAGATION_REQUIRES_NEW事务传播下,方法A调用方法B就是这样,

//        事务A doBegin()
//            事务B doBegin()
//            事务B doCommit()
//        事务A doCommit()
 

而在PROPAGATION_NESTED事务传播下,打了个断点,会发现只会执行一次doBegin和doCommit:

事务A doBegin()
事务A doCommit()

我们用代码输出更加直观。
定义两个方法serviceA和serviceB,使用前者调用后者。前者事务传播使用REQUIRED,后者使用PROPAGATION_NESTED

@Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("测试城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            test2.serviceB();
    }
 @Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
    public void serviceB() {
        Tcity tcity = new Tcity();
        tcity.setId(0);
        tcity.setStateCode("5");
        tcity.setCnCity("测试城市");
        tcity.setCountryCode("ALB");
        tcityMapper.insertSelective(tcity);
        tcityMapper.selectAll2();
        transactionInfo();

这里的transactionInfo()使用事务同步器管理器TransactionSynchronizationManager注册一个事务同步器TransactionSynchronization
这样在事务完成之后afterCompletion会输出当前事务是commit还是rollback,这样也便于测试,比起去刷新数据库看有没有写入,更加方便快捷直观。

同时使用TransactionSynchronizationManager.getCurrentTransactionName()可以得到当前事务的名称,这样可以直观的看到当前方法使用的是同一个事务还是不同的事务。

protected void transactionInfo() {

        String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();
        boolean active = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("transactionName:{}, active:{}", transactionName, active);

        if (!active) {
            log.info("transaction :{} not active", transactionName);
            return;
        }
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_COMMITTED) {
                    log.info("transaction :{} commit", transactionName);
                } else if (status == STATUS_ROLLED_BACK) {
                    log.info("transaction :{} rollback", transactionName);
                } else {
                    log.info("transaction :{}  unknown", transactionName);
                }
            }
        });
    }

执行测试代码:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test { 
    @Autowired
    private Test1 test1;

    @org.junit.Test
    public void test(){
        test1.serviceA();
    }
}

输出:

可以非常直观地观察到3点情况:
1.通过上图标记为1的地方,可以看到两个方法使用了一个事务com.nyp.test.service.propagation.Test1.serviceA
2.通过上图标记为2的地方,以及箭头顺序,可以看到事务执行顺序类似于(事实上不是,只是事务同步器的问题,下文有说明):

//        事务A doBegin()
//            事务B doBegin()
//        事务A doCommit()
//            事务B doCommit()

3.通过事务同步器打印日志发现commit执行了两次。

以上2,3两点与前面打断点的结论貌似是有点冲突。


1.1嵌套事务究竟有几个事务

源码版本:spring-tx 5.3.25

通过源码,可以很直观地观察到,useSavepointForNestedTransaction()默认返回true,这样就不会开启一个新的事务(startTransaction), 而是创建一个新的savepoint

相当于在方法A的时候会开启一个新的事务,在调用方法B的时候,会在方法A之后方法B之前创建一个检查点。

类似于在原来的A方法上手动添加检查点。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Object savePoint = null;
        try {
            Tcity tcity2 = new Tcity();
            tcity2.setId(0);
            tcity2.setStateCode("5");
            tcity2.setCnCity("测试城市2");
            tcity2.setCountryCode("ALB");
            tcityMapper.insertSelective(tcity2);
            transactionInfo();
            savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        }
    }


 

然后通过检查点,将一个逻辑事务分为多个物理事务
我这可不是在乱讲啊,我是有备而来。

Doc: Nested declarative transaction management leads to UnexpectedRollbackException in case of a silent inner setRollbackOnly call [SPR-3452] · Issue #8135 · spring-projects/spring-framework · GitHub
上面是spring 在github官方社区07年的一个贴子,Juergen Hoeller有一段回复。

Juergen Hoeller是谁?他是spring的联合创始人,事务这一块的主要开发者。
 

PROPAGATION_NESTED的不同之处在于,它使用具有多个保存点的单个物理事务,可以回滚到这些保存点。这种部分回滚允许内部事务范围触发其范围的回滚,而外部事务可以继续进行物理事务,尽管已经回滚了一些操作。这通常映射到JDBC保存点上,因此只适用于JDBC资源事务(Spring的DataSourceTransactionManager)。

在嵌套事务中,整体是一个逻辑事务,通过savepoint在jdbc物理层面把调用方法分割成一个个的物理事务。
因为spring层面只有一个逻辑事务,所以通过断点只执行了一次doBegin()和doCommit(),但实际上执行了两次preCommit(),如果有savepoint那就不执行commit(),
这也能回答上面2,3两点问题的疑问。

所以上面方法A调用方法B进行嵌套事务,右(下)图比左(上)图更形象准确:
 

1.2 savepoint

savepoint是JDBC的一种机制,spring运用savepoint来实现了嵌套事务。
在数据库操作中,默认autocommit为true,意味着一条SQL一个事务。也可以将autocommit设置为false,将多条SQL组成一个事务,一起commit或者rollback。
以上都是常规操作,在一个事务中所以数据库操作全部捆绑在一起。在某些特定情况下,在一个事务中,用户只希望rollback其中某部份,这时候可以用到savepoint。

记我们忘掉@Transactional,以编程式事务的方式来手动设置一个savepoint。

方法A,写入一条用户记录,并设置一个检查点。

    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    public void serviceA(){
        TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
        Object savePoint = null;
        try {
            Person person = new Person();
            person.setName("张三");
            personDao.insertSelective(person);
            transactionInfo();
            // 设置一个savepoint
            savePoint = status.createSavepoint();
            test2.serviceB();
        } catch (Exception exception) {
            exception.printStackTrace();
            // 这里输出两次commit,到rollback到51行,会插入一条数据
            status.rollbackToSavepoint(savePoint);
            // 这里会两次rollback
//            platformTransactionManager.rollback(status);

        }
        platformTransactionManager.commit(status);
    }

方法B写入一条日志记录。并在此模拟一个异常。

    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);     
        int a = 1 / 0;
    }

测试希望达到的效果是,日志写入失败,但用户记录写入成功。很明显,如果不使用savepoint是达不到的。因为两个方法是一个事务,在方法B中报错了,抛出异常,用户和日志的数据库操作都将回滚。

测试输出日志:

[2023-04-24 14:40:18.740] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transactionName:null, active:true
[2023-04-24 14:40:18.742] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transactionName:null, active:true
java.lang.ArithmeticException: / by zero
	......省略
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transaction :null commit
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transaction :null commit

数据库也表明用户写入成功,日志写入失败。


2.一开始的问题,B先回滚A再正常提交?

本文开始的问题是方法A事务传播为PROPAGATION_REQUIRED,方法B事务传播为PROPAGATION_NESTED。方法A调用B,methodA正常,methodB抛异常。
这种情况下会发生什么?

B先回滚,A再正常提交这种说法为什么会有问题,有什么问题?

2.1 先B后A的顺序有问题吗?

通过前面事务同步器打印的日志我们得知,事务以test1.serviceA()执行doBegin(),test2.serviceB()执行doBegin(),test1.serviceA()执行doCommit(),test2.serviceB()执行doCommit()这样的顺序执行。
 


但是果真如此吗?

通过源码我们首先得知,preCommit()在commit()方法之前,在preCommit()会做savepoint的判断,如果有检查点就不执行commit()。

  1. 同时方法B只是一个savepoint不是一个真正的事务,并不会执行事务同步器。

  2. 方法A是一个真正的事务,所以会执行commit(),同时也会执行上面的事务同步器。


这里的事务同步器是一个Arraylist,它的执行顺序即是arraylist的遍历顺序,仅仅只代表加入的先后,并不代表事务真正commit/rollback的顺序。


从1,2两点可以得出结论,先B后A的顺序并没有问题。

同时,根据1,在嵌套事务中使用事务同步器要特别小心,在检查点的时候并不会执行同步器,同时会掩盖真正的操作。

比如方法B回滚了,但因为方法B只是个savepoint,所以事务同步器不会执行。等到方法A执行完操作事务同步器的时候,也只会反应外层事务即方法A的事务结果。


2.2 真正的问题

如果B回滚,A是commit还是rollback取决于方法A是否继续把异常往上抛。

让我们先暂时忘掉嵌套事务,测试一个REQUIRES_NEW的案例。
同样的方法A事务传播为REQUIRES,方法B为REQUIRES_NEW
此时方法A和方法B为两个彼此独立的事务。
方法A调用方法B,方法B抛出异常。
此时,方法B肯定会回滚,但方法A呢?按理说彼此独立,那肯定是commit了。


但真的如此吗?


(1). 方法A不做异常处理。

测试结果:

可以看到确实是两个事务,但两个事务都rollback了。因为方法A虽然没有报异常,但它接到了方法B的异常且往上抛了,spring只会认为方法A同样也抛出了异常。因此两个事务都需要回滚。

(2).方法A处理了异常。

将方法A代码try-catch住,再执行。

日志有点多不做截图,

[2023-04-24 16:10:30.669] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transactionName:com.nyp.test.service.propagation.Test1.serviceA, active:true
[2023-04-24 16:10:30.672] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transactionName:com.nyp.test.service.propagation.Test2.serviceB, active:true
[2023-04-24 16:10:30.687] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transaction :com.nyp.test.service.propagation.Test2.serviceB rollback
java.lang.ArithmeticException: / by zero
	 省略
[2023-04-24 16:10:30.689] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transaction :com.nyp.test.service.propagation.Test1.serviceA commit

可以看到两个单独的事务,事务B回滚了,事务A提交了。 
 

虽然我们这小节说的是REQUIRES_NEW,但嵌套事务是一样的道理。

如果B回滚,当方法A继续往上抛异常,则A回滚;当方法A处理了异常不往上抛,则A提交。

3. 场景

在2.2小节中,我们举了REQUIRES_NEW的例子来说明,有的同学可能就会有点疑问了。既然事务B回滚了,事务A都要根据情况来判断是否回滚,那这样嵌套事务跟REQUIRES_NEW有啥区别?

还是拿注册的场景来说。往数据库写1条用户记录,再写1条注册成功操作日志。

  1. 如果日志写入失败,用户写入不受影响。这种情况下, REQUIRES_NEW和嵌套事务都能实现。而且很明显REQUIRES_NEW还没那么弯弯绕绕。
    2.考虑另外一种情况,如果用户写入失败了,那这时候我想要日志写入也失败。因为用户都没了,就不存在注册操作成功的操作日志了。

这种场景,在方法B为REQUIRES_NEW模式下,打印输出

 

可以看到方法B提交了,也就是说用户注册失败了,但用户注册成功的操作日志却写入成功了。

我们再来看看嵌套事务的情况下:
方法A传播级别为REQUIRED,并模拟一个异常。

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA(){
        Person person = new Person();
        person.setName("李四");
        personDao.insertSelective(person);
        transactionInfo();
        test2.serviceB();
        int a = 1 / 0;
    }

方法B事务传播级别为NESTED。

    @Transactional(propagation = Propagation.NESTED)
    public void serviceB() {
        TLog tLog = new TLog();
        tLog.setOprate("user");
        transactionInfo();
        tLogDao.insertSelective(tLog);
    }

执行日志

可以看到同一个逻辑事务下的两段物理事务都回滚了,达到了我们预期的效果。

4.小结

1.方法A事务传播为REQUIRED,方法B事务传播为NESTED。方法A调用方法B,当B抛出异常时,
如果A处理了异常,此时事务A提交。否则,事务A回滚。

2.REQUIRED_NEW和NESTED在有些场景下可以实现相同的功能,但在某些特定场景下只能NESTED实现。
3.NESTED底层逻辑是JDBC的savepoint。父事务类似于一个逻辑事务,savepoint将各方法分割了若干物理事务。
4.在嵌套事务中使用事务同步器时需要特别小心。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/900180.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用chatgpt将中文翻译成学术英语

使用chatgpt将中文翻译成学术英语 方式一 使用chatgpt翻译 你是一个英文学术论文写作专家,以下是一篇学术论文中的一段内容,请先对其进行翻译为英文,并将此部分润色以满足学术标准,提高语法、清晰度和整体可读性,给…

408反向改考自命题的211学校,计算机招生近500人!今年能捡到漏吗?

贵州大学(C) 考研难度(☆☆☆) 内容:23考情概况(拟录取和复试分析)、院校概况、23专业目录、23复试详情、各专业考情分析。 正文1498字,预计阅读:3分钟。 2023考情概况 贵州大学计算机相关各…

基于SpringCloud的会议室预约系统Java基于微服务的会议室报修系统【源码+lw】

💕💕作者:计算机源码社 💕💕个人简介:本人七年开发经验,擅长Java、微信小程序、Python、Android、大数据等,大家有这一块的问题可以一起交流! 💕&#x1f495…

.net通过S7.net读写西门子PLC中,字符串,bool,整数,小数及byte型

注:.net中通过TCP/IP方式通过S7.net.dll动态库,连接到西门子PLC,西门子程序中许勾选优化块,程序读取需要 db块号偏移量 一。使用VS项目,在项目中添加S7.net动态库 代码中引用S7.net动态库 using S7.Net; 实例化PLC服…

Linux网络编程:Socket套接字编程

文章目录: 一:定义和流程分析 1.定义 2.流程分析 3.网络字节序 二:相关函数 IP地址转换函数inet_pton inet_ntop(本地字节序 网络字节序) socket函数(创建一个套接字) bind函数(给socket绑定一个服务器地址结…

使用线性回归模型优化权重:探索数据拟合的基础

文章目录 前言一、示例代码二、示例代码解读1.线性回归模型2.MSE损失函数3.优化过程4.结果解读 总结 前言 在机器学习和数据科学中,线性回归是一种常见而重要的方法。本文将以一个简单的代码示例为基础,介绍线性回归的基本原理和应用。将使用Python和Nu…

安卓框架中的常见问题汇总

目录 1.安卓操作系统的组件结构图如下 2.问题汇总 1.安卓操作系统的组件结构图如下 2.问题汇总 问题1:安卓框架中的库和应用程序框架之间什么关系? 在安卓系统中,应用程序框架层(Application Framework)是核心应用程…

迈向通用听觉人工智能!清华电子系、火山语音携手推出认知导向的听觉大语言模型SALMONN

日前,清华大学电子工程系与火山语音团队携手合作,推出认知导向的开源听觉大语言模型SALMONN (Speech Audio Language Music Open Neural Network)。 大语言模型 SALMONN LOGO 相较于仅仅支持语音输入或非语音音频输入的其他大模型,SALMONN对…

Python爬虫的scrapy的学习(学习于b站尚硅谷)

目录 一、scrapy  1. scrapy的安装  (1)什么是scrapy  (2)scrapy的安装 2. scrapy的基本使用  (1)scrap的使用步骤  (2)代码的演示 3. scrapy之58同城项目结构和基本方法&…

面试最常问的数组转树,树转数组 c++ web框架paozhu实现

刚毕业同学,找工作常被问 二维数组转树,树转二维数组 需要支持无限层级实现,如果你了解这个语言那么实现起来还要一番思考 c web框架 paozhu使用 需要实现数据库表数据到前台菜单实现,就是这种功能 二维数组转树,树转…

学习笔记:Opencv实现拉普拉斯图像锐化算法

2023.8.19 为了在暑假内实现深度学习的进阶学习,Copy大神的代码,记录学习日常 图像锐化的百科: 图像锐化算法-sharpen_lemonHe_的博客-CSDN博客 在环境配置中要配置opencv: pip install opencv-contrib-python Code and lena.png…

【模拟集成电路】反馈系统——基础到进阶(一)

【模拟集成电路】反馈系统——基础到进阶 前言1 概述2 反馈电路特性2.1增益灵敏度降低2.2 终端阻抗变化2.3 带宽拓展2.4 非线性减小 3 放大器分类4 反馈检测和返回机制4.1 按照检测物理量分类4.2 按照检测拓扑连接分类 5 反馈结构分析6 二端口方法7 波特方法6 麦德布鲁克方法 前…

Go中的有限状态机FSM的详细介绍 _

1、FSM简介 1.1 有限状态机的定义 有限状态机(Finite State Machine,FSM)是一种数学模型,用于描述系统在不同状态下的行为和转移条件。 状态机有三个组成部分:状态(State)、事件(…

认识Spring框架

目录 1.了解Spring框架 2.了解Spring的体系结构 3.认识Spring家族 4.实现第一个Spring入门程序 1.了解Spring框架 1.什么是Spring框架? Spring是一个轻量级的控制反转(IoC)和面向切面的容器框架。 关键词概念解释: 1.轻量级…

2023国赛数学建模思路 - 案例:ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法,就是频繁模…

2028量产?兰博基尼首款纯电车型Lanzador亮相,双电机四驱跨界GT

经过多次预热之后,兰博基尼的首款纯电车型Lanzador终于在8月19日正式亮相。这款车以较完整的面貌出现在大众面前,将于2028年开始正式量产。虽然Lanzador仍是一个暂定名字,但它来自西班牙语,意为“投手”、“发射器”和“推动者”&…

C++笔记之注册的含义

C笔记之注册的含义 code review! 文章目录 C笔记之注册的含义1.注册对象到Qt的信号槽系统中2.注册函数到Qt的元对象系统中元对象系统例1例2 3.注册自定义类型到C STL容器中4.将函数指针传递给另一个类,注册回调函数class ICallback存在的意义例1,用于说…

专业课只考2门,计算机学硕最低分290的江苏院校

南京工业大学 考研难度(☆) 内容:23考情概况(拟录取和复试分析)、专业目录、23复试详情、各专业考情分析。 正文1332字,预计阅读:3分钟。 2023考情概况 南京工业大学计算机相关各专业复试和…

Intellij中直接运行ts配置:run configuration for typescript

在Intellij中可以借助插件run configuration for typescript直接运行typescript: run configuration for typescript插件本质还是依赖于 ts-node 来运行,只是其可以帮助我们自动配置好 ts-node 运行参数,简化使用。 第一步:安装…

uniapp配置添加阿里巴巴图标icon流程步骤

文章目录 下载复制文件到项目文件夹里项目配置目录结构显示图标 下载 阿里巴巴icon官网 https://www.iconfont.cn/ 复制文件到项目文件夹里 项目配置目录结构 显示图标