发现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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
4天前
|
SQL Java 关系型数据库
【SpringFramework】Spring事务
本文简述Spring中数据库及事务相关衍伸知识点。
34 9
|
11天前
|
Java 开发者 Spring
理解和解决Spring框架中的事务自调用问题
事务自调用问题是由于 Spring AOP 代理机制引起的,当方法在同一个类内部自调用时,事务注解将失效。通过使用代理对象调用、将事务逻辑分离到不同类中或使用 AspectJ 模式,可以有效解决这一问题。理解和解决这一问题,对于保证 Spring 应用中的事务管理正确性至关重要。掌握这些技巧,可以提高开发效率和代码的健壮性。
41 13
|
1月前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
49 2
Spring高手之路26——全方位掌握事务监听器
|
1月前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
2月前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
64 1
Spring高手之路24——事务类型及传播行为实战指南
|
2月前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
|
2月前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
81 3
|
3月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
49 2
|
3月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
3月前
|
监控 Java 数据库
Spring事务中的@Transactional注解剖析
通过上述分析,可以看到 `@Transactional`注解在Spring框架中扮演着关键角色,它简化了事务管理的复杂度,让开发者能够更加专注于业务逻辑本身。合理运用并理解其背后的机制,对于构建稳定、高效的Java企业应用至关重要。
85 0
下一篇
开通oss服务