事情起因是,摸鱼的时候在某平台刷到一篇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. Excel 去除合并并保留原值的办法

    部分Excel中,对行进行了合并.这个方便展示,但是筛选后数据展示会出现问题,需要去除合并,并在每行中保留原来的值. 1.先选择整行,并"取消单元格合并" 操作后出现大量的空值行. ...

  2. Blog作业02

    目录 前言 设计与分析 踩坑心得 改进建议 总结 前言 这三次作业的题目数量虽然增多,但是在题量加大的同时,这三次作业集的难度也相应的下去了,难度降低的同时也保证了作业集题目的质量.这三次的作业的知识 ...

  3. VBA中的结构体

    结构体必须放在"模块"中: Type Org tag As String person As New Collection End Type 使用: Sub testType() ...

  4. 《Unix/Linux系统编程》第十二周学习笔记

    <Unix/Linux系统编程>第十二周学习笔记 MySQL数据库简介 MySQL是一个关系型数据库管理系统,是最流行的关系型数据库管理系统之一.在 WEB 应用方面,MySQL 是最好的 ...

  5. You need to run build with JDK or have tools.jar on the classpath.If this occures during eclipse build make sure you run eclipse under JDK as well 错误

    我打开项目报错是这样的  pom.xml jdk配置什么的都是好的    但是还是报错 解决错误 : 1.打开你eclipse的根目录,找到eclipse.ini  这个文件夹打开 2.打开是这个样子 ...

  6. flask orm 操作方法

    数据库操作 常用的查询过滤器 过滤器 说明 filter() 把过滤器添加到原查询上,返回一个新查询 filter_by() 把等值过滤器添加到原查询上,返回一个新查询 limit() 使用指定的值限 ...

  7. Lucene搜索引擎-搜索

    Lucene搜索引擎-搜索 常用的Query: BooleanQuery:多个条件组合查询,注意 new BooleanQuery().add(Query, BooleanClause.Occur); ...

  8. 发布jar包到远程仓库 (maven deploy)

    背景: 项目有开放服务模块,现有个需求,需要把开放服务提供成一个jar包,用户可以直接对接. 流程: 1.在pom.xml文件添加distributionManagement节点,将项目打包上传到私服 ...

  9. Shiro权限管理框架-@RequiresPermissions 注解 使用问题记录

    背景: 需要在springboot项目里面用到shiro的权限管理,Shiro访问控制流程:先shiro认证(登录时调用) 然后 shiro授权,但是项目里面登录的功能用的公司统一的系统,所以需要&q ...

  10. MS-08-067 windows smb服务 远程命令执行漏洞

    漏洞概要 MS-08-067是Windows平台中smb服务445端口的远程代码执行漏洞 利用成功可以远程控制主机 影响范围为:windows2000.xp.server 2003.server 20 ...