当Transactional碰到锁,有个大坑,要小心。 (下)

简介: 当Transactional碰到锁,有个大坑,要小心。 (下)

找答案


答案还是在这个类里面:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction


微信图片_20220428160503.png


前面我们聊事务开启的时候,说的是第 382 行代码。

然后 try 代码块里面执行的是我们的业务代码。

现在,我们要研究事务的提交了,所以主要看我框起来的地方。

首先 catch 代码块里面,392 行,看方法名称已经非常的见名知意了:

completeTransactionAfterThrowing 在抛出异常之后完成事务的提交。

你看我的代码,只是用到了 @Transactional 注解,并没有指定异常。

那么问题就来了:

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

如果你不知道答案,就可以带着问题去看源码。

如果你知道答案,但是没有亲眼看到对应的代码,那么也可以去寻找源码。

如果你知道答案,也看过这部分源码,温故而知新。

先说答案:默认回滚的异常是 RuntimeException 或者 Error

我只需要在业务代码里面抛出一个 RuntimeException 的子类,比如这样的:


image.png


只需要往下调试几步,你就能走到这个方法来:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn


image.png


发现这个 winner 对象为空,接着走了这个逻辑:

return super.rollbackOn(ex);

答案就藏着这行代码的背后:


image.png


如果返回为 false,则表示不需要回滚,调用 commit 方法:


image.png


那么怎么让它返回 false 呢?

很简单嘛,这样一搞就好了:


image.png


框架给你留了口子,你就把它用起来。

当我把代码改成上面那样,然后重新启动项目,再次访问代码。

我们去寻找出现指定异常不回滚的具体的实现逻辑在哪。

其实也在我们刚刚看到的方法里面:


image.png


你看,这个时候 winner 不为 null 了。它是一个 NoRollbackRuleAttribute 对象了。

所以就走入这行代码,返回 false 了:

return !(winner instanceof NoRollbackRuleAttribute);

于是,就成功走到了 else 分支里面,出了异常也 commit 了,你说神奇不神奇:


image.png


写到这里的时候,我突然想到了一个骚操作,甚至有可能变成一道沙雕面试题:


image.png


这个操作骚不骚,到底会回滚呢还是不回滚呢?


image.png


如果你在项目里看到这样的代码肯定是要骂一句傻逼的。

但是面试官就喜欢搞这些阴间的题目。

我想到这个问题的时候,我也不知道答案是什么,但是我知道答案还是在源码里面:


image.png


首先,从结果上可以直观的看到,经过 for 循环之后, winner 是 RollbackRuleAttribute 对象,所以下面的代码返回 true,需要回滚:

return !(winner instanceof NoRollbackRuleAttribute);

问题就变成了 winner 为什么经过 for 循环之后是 RollbackRuleAttribute?

答案需要你自己去调试一下,很容易就明白了,我描述起来比较费劲。

简单一句话:导致 winner 是 RollbackRuleAttribute 的原因,就是因为被循环的这个 list 是先把 RollbackRuleAttribute 对象 add 了进去。

那么为什么 RollbackRuleAttribute 对象先加入到集合呢?

org.springframework.transaction.annotation.SpringTransactionAnnotationParser#parseTransactionAnnotation(org.springframework.core.annotation.AnnotationAttributes)


image.png


别问,问就是因为代码是这样写的。

为什么代码要这样写呢?

我想可能设计这块代码的开发人员觉得 rollbackFor 的优先级比 noRollbackFor 高吧。

再来一个问题:

Spring 源码怎么匹配当前这个异常是需要回滚的?

别想那么复杂,大道至简,直接递归,然后一层层的找父类,对比名称就完事了。


image.png


你注意截图里面的注释:

一个是 Found it!

表示找到了,匹配上了,用了感叹号表示很开心。

一个是 If we've gone as far as we can go and haven't found it...

啥意思呢,这个 as far as 在英语里面是一个连词,表示“直到..为止..”的意思。引导的是状语从句,强调的是程度或范围。

所以,上面这句话的意思就是:

如果我们已经走到我们能走的最远的地方,还没匹配上,代码就只能这样写了:


image.png


异常类,最远的地方就是 Throwable.class。没匹配上,就返回 -1。

好了,通过两个没啥卵用的知识点,顺带学了点实战英语,关于业务代码出了异常回滚还是提交这一块的代码就差不多了。

但是我还是建议大家亲自去 Debug 一下,可太有意思了。

然后我们接着聊正常场景下的提交。


image.png


这个代码块里面,try 我们也聊了,catch 我们也聊了。

就差个 finally 了。

我看网上有的文章说 finally 里面就是 commit 的地方。

错了啊,老弟。

这里只是把数据库连接给重置一下。

方法上已经给你说的很清楚了:


image.png


这样只有 SQL 语句是 commit 的时候才会停下来。

又一个调试小细节,送给你,不客气。

现在,我们知道原因了,那我现在把代码稍微变一下:


image.png

说没有问题的同学请好好反思一下。

这个地方的原理和前面讲的东西是一模一样的呀,肯定也是有问题的。

这个加锁方式就是错误的。

所以你记住了,以后面试官问你 @Transactional 的时候,你把标准答案先背一遍之后,如果你对锁这块的知识点非常的熟悉,就可以在不经意间说一下结合锁用的时候的异常场景。

别说你写的,就说你 review 代码的时候发现的,深藏功与名。

另外记得扩展一下,现在都是集群服务了,加锁得上分布式锁。

但是原理还这个原理。

既然都聊到分布式锁了,这和面试官又得大战几个回合。

是你主动提起的,把面试官引到了你的主战场,拿几分,不过分吧。

一个面试小技巧,送给你,不客气。

image.png


解决方案


现在我们知道问题的原因了。

解决方案其实都呼之欲出了嘛。

正确的使用锁,把整个事务放在锁的工作范围之内:

image.png


说对的同学,今天就先到这里,请回去等通知啊。

别被带到沟里去了呀,朋友。

你仔细想想这个事务会生效吗?

提示到这里还没想明白的同学,赶紧去搜一下事务失效的几种场景。

我这里说一个能正常使用的场景:


image.png

只是这种自己注入自己的方式,我觉得很恶心。

如果项目里面出现了这样的代码,一定是代码分层没有做好,项目结构极其混乱。

不推荐。

还可以使用编程式事务的方式去写,自己去控制事务的开启、提交、回滚。

比直接使用 @Transactional 靠谱。

除此之外,还有一个骚一点的解决方案。

image.png


好啥啊?

串行化性能跟不上啊!

这玩意太悲观了,对于同一行的数据,读和写的时候都会进行加锁操作。当读写锁出现冲突的时候,后面来的事务就排队等着。

这个骚操作,知道就行了,别用。

你就当是一个没啥卵用的知识点就行了。

但是,如果你们是一个不追求性能的场景,这个没有卵用的知识点就变成骚操作了。


rollback-only


前面提到了这个 rollback-only,为了更好的行文,所以我一句话就带过了,其实它也是很有故事的,单独拿一节出来简单说一下,给大家模拟一下这个场景。

以后你见到这个异常就会感觉很亲切。

Spring 的事务传播级别默认是 REQUIRED,含义是如果当前没有事务,就新建一个事务,如果上下文中已经有一个事务,则共享这个事务。

直接上代码:

image.png

这里有 sellProduct、sellProductBiz 两个事务,sellProductBiz 是内层事务,它会抛出了异常。

当执行整个逻辑的时候,会抛出这个异常:

Transaction rolled back because it has been marked as rollback-only

image.png


image.png


所以,我们只需要分析这个 if 条件为什么满足了,就大概摸清楚脉络了。

if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())

前面的 shouldCommitOnGlobalRollbackOnly 默认为 false:


image.png


问题就精简为了:defStatus.isGlobalRollbackOnly() 为什么是true?

为什么?

因为 sellProductBiz 抛出异常后,会调用 completeTransactionAfterThrowing 方法执行回滚逻辑。

肯定是这个方法里面搞事情了啊。

org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback


image.png


在这里,把链接的 rollbackOnly 置为了 true。

所以,后面的事务想要 commit 的时候,一检查这个参数,哦豁,回滚吧。

大概就是这样的:


image.png

如果这不是你期望的异常,怎么解决呢?

理解了事务的传播机制就简单的一比

image.png


就这样,新开个事务,跑起来没毛病,互不干扰。


image.png


最后说一句


好了,看到了这里安排个关注吧,周更原创很累的,需要一点正反馈。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,你也可以叫我小歪,一个主要写代码,经常写文章,偶尔拍视频的程序猿。

目录
相关文章
|
3月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
110 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
|
5月前
|
存储 Java 关系型数据库
Spring事务失效的 8 大原因,这次可以吊打面试官了!
Spring事务失效的 8 大原因,这次可以吊打面试官了!
|
Java 开发者 Spring
聊聊spring事务失效的12种场景,太坑了(下)
聊聊spring事务失效的12种场景,太坑了
聊聊spring事务失效的12种场景,太坑了(下)
|
消息中间件 JavaScript 小程序
多线程如何实现事务回滚?一招帮你搞定!
多线程如何实现事务回滚?一招帮你搞定!
多线程如何实现事务回滚?一招帮你搞定!
|
Java 测试技术 数据库
【事务与锁】当Transactional遇上synchronized
最近工作中遇到某些七七八八的问题,就是与事务和锁、并发都有着紧密联系相关的问题所在。主要情况是:通过调用方法获取编号,而这个编号是递增有序的,并且存在于数据库中,简单理解就是需要用到这种编号(以下称任务编号),需要从数据库获取出来,在+1最为本次需要的编号,然后在存回数据库中,提供下次使用。
917 0
【事务与锁】当Transactional遇上synchronized
|
存储 缓存 Java
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
173 0
|
消息中间件 JavaScript 小程序
如何将 @Transactional 事务注解运用到炉火纯青?
如何将 @Transactional 事务注解运用到炉火纯青?
|
SQL 消息中间件 JavaScript
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?
|
存储 SQL Java
聊一聊使用事务时(@Transactional)可能出现的问题
聊一聊使用事务时(@Transactional)可能出现的问题
322 0
聊一聊使用事务时(@Transactional)可能出现的问题
|
Java 关系型数据库 MySQL
面试突击83:什么情况会导致@Transactional事务失效?
面试突击83:什么情况会导致@Transactional事务失效?
177 0