发现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日志并进行多维度分析。
目录
相关文章
|
2月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
1月前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
1月前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
74 4
Spring事务传播机制(最全示例)
|
16天前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
18 2
|
17天前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
28天前
|
Java Spring
Spring 事务传播机制是什么?
Spring 事务传播机制是什么?
19 4
|
19天前
|
监控 Java 数据库
Spring事务中的@Transactional注解剖析
通过上述分析,可以看到 `@Transactional`注解在Spring框架中扮演着关键角色,它简化了事务管理的复杂度,让开发者能够更加专注于业务逻辑本身。合理运用并理解其背后的机制,对于构建稳定、高效的Java企业应用至关重要。
31 0
|
2月前
|
Java 程序员 数据库连接
女朋友不懂Spring事务原理,今天给她讲清楚了!
该文章讲述了如何解释Spring事务管理的基本原理,特别是针对女朋友在面试中遇到的问题。文章首先通过一个简单的例子引入了传统事务处理的方式,然后详细讨论了Spring事务管理的实现机制。
女朋友不懂Spring事务原理,今天给她讲清楚了!
|
2月前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
2月前
|
前端开发 Java 数据库连接
一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)
这篇文章总结了Java面试中的十个问题,包括Spring事务传播机制、Spring事务失效条件、Bean自动装配方式、Spring、Spring MVC和Spring Boot的区别、Spring MVC的工作流程和主要组件、Spring Boot的自动配置原理和Starter概念、嵌入式服务器的使用原因,以及MyBatis的优缺点。