事情起因是,摸鱼的时候在某平台刷到一篇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);
}
}

然后通过检查点,将一个逻辑事务分为多个物理事务

我这可不是在乱讲啊,我是有备而来。

https://github.com/spring-projects/spring-framework/issues/8135

上面是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.在嵌套事务中使用事务同步器时需要特别小心。

看到这里点个赞呗`

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

  1. 【spring cloud】spring cloud服务发现注解之@EnableDiscoveryClient与@EnableEurekaClient

    spring cloud服务发现注解之@EnableDiscoveryClient与@EnableEurekaClient的区别

  2. 关于Thinkcmf中热门文章的使用

    今天在做一个首页新闻列表页面的功能时候,因为要读取大量的新闻内容列表.如果每条数据都要从数据按照文章id和term_id来对应取值,无疑是很痛苦的. 然而机智如我,发现cmf框架中热门文章的用法: 在 ...

  3. Spring IOC 容器源码分析系列文章导读

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  4. Spring配置文件中的那些标签意味着什么(持续更新)

    前言 在看这边博客时,如果遇到有什么不清楚的地方,可以参考我另外一边博文.Spring标签的探索,根据这边文章自己来深入源码一探究竟.这里自己只是简单记录一下各标签作用,每个人困惑不同,自然需求也不一 ...

  5. Python实现抓取CSDN热门文章列表

    1.使用工具: Python3.5 BeautifulSoup 2.抓取网站: csdn热门文章列表 http://blog.csdn.net/hot.html 3.分析网站代码: 4.实现代码: _ ...

  6. 事务之六:spring 嵌套事务

    一.基本概念 事务的隔离级别,事务传播行为见<事务之二:spring事务(事务管理方式,事务5隔离级别,7个事务传播行为,spring事务回滚条件) > 二. 嵌套事务示例 2.1.Pro ...

  7. spring cloud服务发现注解之@EnableDiscoveryClient与@EnableEurekaClient区别

    在使用服务发现的时候有两种注解, 一种为@EnableDiscoveryClient, 一种为@EnableEurekaClient, 用法上基本一致,下文是从stackoverflow上面找到的对这 ...

  8. spring cloud服务发现注解之@EnableDiscoveryClient与@EnableEurekaClient

    使用服务发现的时候提到了两种注解,一种为@EnableDiscoveryClient,一种为@EnableEurekaClient,用法上基本一致,今天就来讲下两者,下文是从stackoverflow ...

  9. Spring Cloud 服务发现和消费

    服务的发现和消费 有了服务中心和服务提供者,下面我们来实现一个服务的消费者: 服务消费者主要完成两个任务——服务的发现和服务的消费,服务发现的任务是由Eureka客户端完成,而服务消费的任务是由Rib ...

  10. Spring嵌套事务控制

    A类   callBack_test() B类   testadd() C类   select(),得查询到B类testadd方法中新增的数据.以及初始化一些属性 场景:A类 嵌套 B类  B类嵌套C ...

随机推荐

  1. 第七章 狄克斯特拉算法 (Dijkstra's algorithm)

    步骤 找出最便宜的节点,即可在最短时间内前往的节点 对于该节点的的邻居,检查是否有前往他们的更短路径,如果有,就更新其开销 重复这个过程,知道对图中的每个节点都这样做了 计算最终路径 条件 只适用于有 ...

  2. [C#] Func及Action的快速Demo

    参考代码1: using System;using System.Collections.Generic;using System.Linq;namespace FuncActionDemo{    ...

  3. CGAL的demo运行的步骤

    首先使用CMake,找到demo的源文件目录,并且指定生成文件目录: 点击configur,done 点击generate,done 找到build目录中的.sln 文件,打开 ALL_BUILD 生 ...

  4. 九九乘法表打印记一次al面试

    for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(i + "x&quo ...

  5. layui 手册

    https://layui.yii666.com/doc/modules/layer.html

  6. Flask CURD(增删改查)

    1.创建flask项目 2.修改配置文件: ''' config.py 保存项目配置 ''' 导入Flask模块 from flask import Flask 额外安装: 数据库操作模块 from ...

  7. vsftpd2.3.4 后门笑脸漏洞

    漏洞概要 在vsftpd 2.3.4版本中,在登录输入用户名时输入:)类似于笑脸的符号,会导致服务处理开启6200后门端口,直接执行系统命令 漏洞利用 攻击机:kali 2022 msfconsole ...

  8. 纠删码在实时视频流中的应用丨Dev for Dev 专栏

    本文为「Dev for Dev 专栏」系列内容,作者为声网网络体验团队王瑞. 01 背景 在实时音视频通话中,音视频质量受网络丢包影响较大,特别是对于视频. 为什么视频对丢包更敏感呢?通常来说,音频的 ...

  9. CSP2022-S游寄

    游寄游寄,顾名思义,边游边寄 11.00AM 起床 复习了一下各种终端命令,然后又复习了一下对拍 虽然都没用到 然后接着睡. 有点小紧张,毕竟一年没搞OI 12.00AM 今天吃河虾 还行,只是有点扎 ...

  10. 利用selenium爬取前程无忧招聘数据

    1.背景介绍 selenium通过驱动浏览器,模拟浏览器的操作,进而爬取数据.此外,还需要安装浏览器驱动,相关步骤自行解决. 2.导入库 import csv import random import ...