发现Spring事务的一个实锤bug,官方还拒不承认?你来评评理... (上)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 发现Spring事务的一个实锤bug,官方还拒不承认?你来评评理... (上)

你好呀,我是歪歪。

事情是这样的,上周我正在全神贯注的摸鱼,然后有个小伙伴给我发来微信消息,提出了自己关于事务的一个疑问,并配上两段代码:

image.png

先说结论:我认为这是 Spring 事务的一个 bug。但是官方说这只能算是文档上的缺陷,不能算是代码的 bug。

(好吧,我这篇文章写了好几天,所以我写到上面这一句的时候,官方还不承认是 bug,但是写完之后他们也承认确实是代码缺陷。不影响,接着往下看。)

好家伙,我懂了,一切解释权归官方所有。

在开始刨根问底之前,我想先就关于如何提问这个问题掰扯几句。

我把上面这个读者的问题截出来,是因为我觉得这个提问简直就是模板方法般的提问。

给了一段示例代码、给了一段源码、说明自己的问题、并表明自己已经查询过但是没有找到合适的答案。

我读完他的文字,我很快就能 get 到他的问题是什么,所以我们之间的交流就非常的高效。

最终我们并没有讨论出一个合理的解释,于是他去提了一个 issues,希望能得到官方的比较权威的回答。

所以我们的故事就围绕着这个 issues 开始吧。

image.png


舞台搭建


在正戏开始之前,我先给你把舞台搭建出来,也就是把 Demo 搞出来。

因为是关于 Spring 事务的问题嘛,所以这个 Demo 主要就是体现出“事务”的应用就行了嘛。

所以 Demo 里面最核心的东西就是这个部分:

image.png

假设这里有一个只允许 10-18 岁的用户使用奇怪的网站,这个部分就代表这个网站的用户注册功能。

接着我们往 user_info 表里面插入一条数据,然后判断年龄如果大于 18 岁,那么抛出 AgeExceptionOver18 异常,表示这个用户不是目标用户。

但是你注意,我 @Transactional 注解里面的 rollbackFor 是 AgeException.class,意思是我并不想回滚这个大于 18 岁的用户,我还是想把他的注册信息保存下来,只是抛出一个异常来表示他不是我的目标用户,

而当我们插入一个年龄小于 10 岁的用户的时候,会抛出 AgeException.class,应该把刚刚执行的插入语句给回滚掉,我并不想保存这部分用户的信息。

好的,那么现在就会有小伙伴问了:小于 10 岁的用户既然不想保存,那么为什么不在插入之前判断呢?

很好的问题,实际开发中肯定是要在插入之前判断的,但是我这里只是为了演示事务功能,所以我就这样写了,咋地了吧!


image.png

上面的四个类,就是最关键的几个类,所以我单独拿出来说一下。

整个项目结构也非常的简单:

image.png

其他的类不关键,就不拿出来说了,都是你最拿手的 crud。花五分钟搭一个这个项目出来不过分吧?中间还能摸两分钟的鱼。

我把日志级别调整为 debug 级别,接着把项目跑起来验证一下功能。

然后调用这个链接:

http://127.0.0.1:8085/insertUser?age=8

对应的日志是这样的:

image.png

可以看到我框起来的部分,首先确认执行了 insert 语句,且 age 确实是为 8。但是最后 Rolling back 了,即回滚了。

为什么回滚,我们心里也是门清,因为这里呼应上了:

image.png

接下来试一下 age 为 18 岁的用户:

http://127.0.0.1:8085/insertUser?age=18

对应的日志是这样的:

image.png

这没啥说的,事务成功提交,数据插入成功。是我们预期的结果。

数据库数据也没毛病:

image.png

然后试一下 age 为 28 岁的用户。

这个用户我们的预期是抛出 AgeExceptionOver18 异常,但是数据得插入成功。

来走一个:

http://127.0.0.1:8085/insertUser?age=28

对应的日志是这样的:

image.png

首先数据居然回滚了???

异常倒是抛出来了,但是这也没呼应上啊!

image.png

先不管到底啥原理吧,从我的认知来说,首先我的 @Transactional 注解用法绝对没有错,事务配置没有绝对没有错,我的异常也没有乱抛,你凭什么给我回滚了?

你还说这不是 bug?

just 改改 documentation 就行了?

言外之意是要“抵赖”,强行从文档上找补吗?

image.png

image.png

额...等等,我写这段的时候情况是这样的。

但是等我写完这段,第二天再次进 issues 里面去看,发现事情发生了变化,官方又承认这是一个 bug 了,会在 5.3.x 版本里面修改文档上的描述,会在 6.0 版本里面进行代码上的修复。

image.png

但是我前面已经铺垫了这么多,已经写好了,我就不改了,就当在这里留下一个创作痕迹吧,哈哈。

我们接着往下看。


戏剧冲突


一部戏,肯定有它的戏剧冲突,这是它的核心部分。那么我们 Demo 里面的核心冲突是什么呢?

这一小节就先告诉你“戏剧冲突”在哪。

我先问你一个问题:

Spring 管理的事务,默认回滚的异常是什么呢?

我们带着这个问题去看源码,找到了这个问题的答案,你就能丝滑入戏。

先搞个断点,把程序跑起来,然后看调用栈:

image.png

可以看到调用栈里面和事务相关的有这样一个方法:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

这就是我们的突破口。

什么,你问我怎么一下就找到了这里来的?

我只能说:熟能生巧而已。

好吧,其实是有技巧的,你可以自己试着去找一下,因为这不是本文重点,所以我就不多说了。

方法执行异常之后,会走到 catch 代码块里面,下面这一行代码就是异常相关处理的入口:

image.png

在我们 age=28 的这个场景下,这个方法进来之后,首先 ex 参数就是我们自定义的 AgeExceptionOver18 异常:

image.png

我还框起来了一行代码:

txInfo.transactionAttribute.rollbackOn(ex)

这一行代码你看名字 rollbackOn 也知道是判断 ex 参数是否匹配指定的回滚异常。

如果匹配呢?如果不匹配呢?

image.png

如果匹配,rollback。

如果不匹配,commit。

好了,我们接着往下看。

你会走到这里来:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn


image.png

而这个方法,当 winner 为 null 的时候,上面有个注释,是说没有匹配到对应的规则。

也就是我们什么都不配置,默认情况下,winner 就是 null。

那么下面这行代码里面就藏着我们要找的问题的答案:

return super.rollbackOn(ex);


image.png

所以 Spring 管理的事务,默认回滚的异常是什么呢?

源码告诉我:如果当前抛出的异常属于 RuntimeException 或者 Error 都会回滚。

前面都是我在铺路,只是为了把你引到 rollbackOn 方法这个地方来,甚至 super.rollbackOn(ex) 这行代码都是烟雾弹,本文中我们完全不必关注。

image.png

image.png

首先我们明确一下这个 rollbackRules 是啥玩意。

在我们的 Demo 里面,它就是我们配置在 @Transactional 注解上 rollbackFor 属性中的 AgeException.class 对象的全路径名称:

image.png

image.png

重点关注这一行代码:

int depth = rule.getDepth(ex);

来,看一下 rule 和 ex 分别是什么东西:

image.png

rule 里面的 exceptionName 是我们配置的 AgeException 对象的全路径名称。

ex 是我们程序抛出的 AgeExceptionOver18 异常。

这场戏的核心冲突,就藏在这里的这个方法里面:

org.springframework.transaction.interceptor.RollbackRuleAttribute#getDepth(java.lang.Throwable)


image.png

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
3月前
|
SQL Java 关系型数据库
Spring事务传播机制:7种姿势教你玩转"事务接力赛"
事务传播机制是Spring框架中用于管理事务行为的重要概念,它决定了在方法调用时事务如何传递与执行。通过7种传播行为,开发者可以灵活控制事务边界,适应不同业务场景。例如:REQUIRED默认加入或新建事务,REQUIRES_NEW独立开启新事务,NESTED支持嵌套回滚等。合理使用传播机制不仅能保障数据一致性,还能提升系统性能与健壮性。掌握这“七种人格”,才能在复杂业务中游刃有余。
|
9月前
|
Java Spring
Spring中事务失效的场景
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 , 如果使用的是被代理对象调用, 那么@Transactional会失效 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
|
4月前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
9月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
228 0
|
9月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
1164 0
|
9月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
246 0
|
7月前
|
人工智能 Java 数据库连接
Spring事务失效场景
本文深入探讨了Spring框架中事务管理可能失效的几种常见场景及解决方案,包括事务方法访问级别不当、方法内部自调用、错误的异常处理、事务管理器或数据源配置错误、数据库不支持事务以及不合理的事务传播行为或隔离级别。通过合理配置和正确使用`@Transactional`注解,开发者可以有效避免这些问题,确保应用的数据一致性和完整性。
395 10
|
6月前
|
Java 关系型数据库 MySQL
【Spring】【事务】初学者直呼学会了的Spring事务入门
本文深入解析了Spring事务的核心概念与使用方法。Spring事务是一种数据库事务管理机制,通过确保操作的原子性、一致性、隔离性和持久性(ACID),维护数据完整性。文章详细讲解了声明式事务(@Transactional注解)和编程式事务(TransactionTemplate、PlatformTransactionManager)的区别与用法,并探讨了事务传播行为(如REQUIRED、REQUIRES_NEW等)及隔离级别(如READ_COMMITTED、REPEATABLE_READ)。
478 1
|
9月前
|
SQL Java 数据库连接
Spring中的事务是如何实现的
1. Spring事务底层是基于数据库事务和AOP机制的 2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean 3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解 4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接 5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮ 常重要的⼀步 6. 然后执⾏当前⽅法,⽅法中会执⾏sql 7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务 8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务
|
9月前
|
JavaScript Java 开发者
Spring事务失效,常见的情况有哪些?
本文总结了Spring事务失效的7种常见情况,包括未启用事务管理功能、方法非public类型、数据源未配置事务管理器、自身调用问题、异常类型错误、异常被吞以及业务和事务代码不在同一线程中。同时提供了两种快速定位事务相关Bug的方法:通过查看日志(设置为debug模式)或调试代码(在TransactionInterceptor的invoke方法中设置断点)。文章帮助开发者更好地理解和解决Spring事务中的问题。
355 7