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

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

你好呀,我是why。

前几天在某平台看到一个技术问题,很有意思啊。

涉及到的两个技术点,大家平时开发使用的也比较多,但是属于一个小细节,深挖下去,还是有点意思的。

来,先带你看一下问题是什么,同时给你解读一下这个问题:

https://segmentfault.com/q/1010000040361592

首先,这位同学给出了一个代码片段:

image.png


他说他有一个 func 方法,这个方法里面干了两件事:

  • 1.先查询数据库里面的商品库存。
  • 2.如果还有库存,那么对库存进行减一操作,模拟商品卖出。

对于第二件事,提问的同学其实写了两个操作在里面,所以我再细分一下:

  • 2.1 对库存进行减一操作。
  • 2.2 在订单表插入订单数据。

很显然,这两个操作都会对数据库进行操作,且应该是应该原子性的操作。

所以,在方法上加了一个 @Transactional 注解。

接着,为了解决并发访问的问题,他用 lock 把整个代码包裹了起来,保证在单体结构下,同一时刻只有一个请求能去执行减少库存,生成订单的操作。

非常的完美。

首先,先把大前提申明一下:MySQL 数据库的隔离机制使用的是可重复读级别。

image.png


这个时候,问题就来了。

如果是高并发的情况下,假设真的就有多个线程同时调用 func 方法。

要保证一定不能出现超卖的情况,那么就需要事务的开启与提交能完整的包裹在 lock 与 unlock之间。

显然事务的开启一定是在 lock 之后的。

故关键在于事务的提交是否一定在 unlock 之前?

如果事务的提交在 unlock 之前,没有问题。

因为事务已经提交了,代表库存一定减下来了,而这个时候锁还没释放,所以,其他线程也进不来。

画个简单的示意图如下:


image.png


等 unlock 之后,再进来一个线程,执行查询数据库的操作,那么查询到的值一定是减去库存之后的值。

但是,如果事务的提交是在 unlock 之后,那么有意思的事情就出现了,你很有可能发生超卖的情况。

上面的图就变成了这样的了,注意最后两个步骤调换了:

网络异常,图片无法展示
|


举个例子。

假设现在库存就只有一个了。

这个时候 A,B 两个线程来请求下单。

A 请求先拿到锁,然后查询出库存为一,可以下单,走了下单流程,把库存减为 0 了。

但是由于 A 先执行了 unlock 操作,释放了锁。

B 线程看到后马上就冲过来拿到了锁,并执行了查询库存的操作。

注意了,这个时候 A 线程还没来得及提交事务,所以 B 读取到的库存还是 1,如果程序没有做好控制,也走了下单流程。

哦豁,超卖了。

所以,再次重申问题:

在上面的示例代码的情况下,如果事务的提交在 unlock 之前,是没有问题的。但是如果在 unlock 之后是会有问题的。

那么事务的提交到底是在 unlock 之前还是之后呢?

这个事情,先把问题听懂了,接着我们先按下不表。你可以简单的思考一下。

image.png


我想先聊聊这句被我轻描淡写,一笔带过,你大概率没有注意到的话:

显然事务的开启一定是在 lock 之后的。

这句话,不是我说的,是提问的同学说的:

image.png


你有没有一丝丝疑问?

怎么就显然了?哪里就显然了?为什么不是一进入方法就开启事务了?

请给我证据。

来吧,瞅一眼证据。


网络异常,图片无法展示
|


事务开启时机


证据,我们需要去源码里面找。

另外,我不得不多说一句 Spring 在事务这块的源码写的非常的清晰易懂,看起来基本上没有什么障碍。

所以如果你不知道怎么去啃源码,那么事务这块源码,也许是你撕开源码的一个口子。

好了,不多说了,去找答案。

答案就藏在这个方法里面的:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin


image.png


先看我下面框起来的那一行日志:

Switching JDBC Connection [HikariProxyConnection@946359486 wrapping com.mysql.jdbc.JDBC4Connection@7a24806] to manual commit

你知道的,我是个技术博主,偶尔教点单词。

Switching,转换。

Connection,链接。

manual commit,手动提交。

Switching ... to ...,把什么转换为什么。

没想到吧,这次学技术的同时不仅学了几个单词,还会了一个语法。


image.png

所以,上面那句话翻译过来就非常简单了:

把数据库连接切换为手动提交。

然后,我们看一下打印这行日志的代码逻辑,也就是被框起来的代码部分。

我单独拿出来:

image.png


逻辑非常清晰,就是把连接的 AutoCommit 参数从 ture 修改为 false。

那么现在问题就来了,这个时候,事务启动了吗?

我觉得没启动,只是就绪了而已。

启动和就绪还是有一点点差异的,就绪是启动之前的步骤。

那么事务的启动有哪些方式呢?

  • 第一种:使用启动事务的语句,这种是显式的启动事务。比如 begin 或 start transaction 语句。与之配套的提交语句是 commit,回滚语句是 rollback。
  • 第二种:autocommit 的值默认是 1,含义是事务的自动提交是开启的。如果我们执行 set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

很显然,在 Spring 里面采用的是第二种方式。

而上面的代码 con.setAutoCommit(false) 只是把这个链接的自动提交关掉。

事务真正启动的时机是什么时候呢?

前面说的 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才算是真正启动。

如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。需要注意的是这个命令在读已提交的隔离级别(RC)下是没意义的,和直接使用 start transaction 一个效果。

回到在前面的问题:什么时候才会执行第一个 SQL 语句?

就是在 lock 代码之后。

所以,显然事务的开启一定是在 lock 之后的。

这一个简单的“显然”,先给大家铺垫一下。

接下来,给大家上给动图看一眼,更加直观。

首先说一下这个 SQL:

select * from information_schema.innodb_trx;

不多解释,你只要知道这是查询当前数据库有哪些事务正在执行的语句就行。

你就注意看下面的动图,是不是第 27 行查询语句执行完成之后,查询事务的语句才能查出数据,说明事务这才真正的开启:

image.png

最后,我们把目光转移到这个方法的注释上:


image.png


写这么长一段注释,意思就是给你说,这个参数我们默认是 ture,原因就是在某些 JDBC 的驱动中,切换为自动提交是一个很重的操作。

那么在哪设置的为 true 呢?

没看到代码,我一般是不死心的。

所以,一起去看一眼。

setAutoCommit 这个方法有好几个实现类,我也不知道具体会走哪一个:


image.png


所以,我们可以在下面这个接口打上一个断点:

java.sql.Connection#setAutoCommit


image.png


可以看到,默认确实是 true。

等等,你不会真的以为我是想让你看这个 true 吧?

我是想让你知道这个调试技巧啊。

不知道有多少个小伙伴曾经问过我:这个接口实现类好多啊,我怎么知道在哪打断点啊?

我说:很简单啊,就在每个实现类的第一行代码打上断点就好了。

然后他说:别闹,我经常给你的文章一键三联。

我当时就被感动了,既然是这样的好读者,我当然把可以直接在接口上打断点的这个小技巧教给他啦。

image.png

好了,不扯远了。

再说一个小细节,这一小节就收尾。

你再去看这小节的开头,我直接说答案藏在这个方法里面:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

直接把答案告诉你了,隐去了探索的过程。

但是这个东西,就像是数学公式推导一样,省略了一步,就会让人看起来一脸懵逼。

就像下面这个小耗子一样:

image.png


所以,我是怎么知道在这个地方打断点的呢?

答案就是调用栈。

先给大家看一下我的代码:

image.png

啥也先不管,上来就先在 26 行,方法入口处打上断点,跑起来:

微信图片_20220428155938.png

目录
相关文章
|
1月前
|
NoSQL 前端开发 Java
【幂等性大坑】事务提交前释放锁导致锁失效问题
在事务中,使用了 Redis 分布式锁.这个方法一旦执行,事务生效,接着就 Redis 分布式锁生效,代码执行完后,先释放 Redis 分布式锁,然后再提交事务数据,最后事务结束。如果是表单重复提交场景,可以尝试给“订单号”等有唯一性的字段加唯一索引,这样重复提交时会因为唯一索引约束导致索引失效。5、如果表的一个字段,要作为另外一个表的外键,这个字段必须有唯一约束(或是主键),如果只是有唯一索引,就会报错。2、创建唯一约束,会自动创建一个同名的唯一索引,该索引不能单独删除,删除约束会自动删除索引。
【幂等性大坑】事务提交前释放锁导致锁失效问题
|
2月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
85 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
|
4月前
|
存储 Java 关系型数据库
Spring事务失效的 8 大原因,这次可以吊打面试官了!
Spring事务失效的 8 大原因,这次可以吊打面试官了!
|
5月前
|
监控 安全 Java
Spring注解之恋:@Async和@Transactional的双重奏
Spring注解之恋:@Async和@Transactional的双重奏
747 0
|
Java 开发者 Spring
聊聊spring事务失效的12种场景,太坑了(下)
聊聊spring事务失效的12种场景,太坑了
聊聊spring事务失效的12种场景,太坑了(下)
|
消息中间件 JavaScript 小程序
多线程如何实现事务回滚?一招帮你搞定!
多线程如何实现事务回滚?一招帮你搞定!
多线程如何实现事务回滚?一招帮你搞定!
|
Java 测试技术 数据库
【事务与锁】当Transactional遇上synchronized
最近工作中遇到某些七七八八的问题,就是与事务和锁、并发都有着紧密联系相关的问题所在。主要情况是:通过调用方法获取编号,而这个编号是递增有序的,并且存在于数据库中,简单理解就是需要用到这种编号(以下称任务编号),需要从数据库获取出来,在+1最为本次需要的编号,然后在存回数据库中,提供下次使用。
876 0
【事务与锁】当Transactional遇上synchronized
|
存储 缓存 Java
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
每日一博 - 常见的Spring事务失效&事务不回滚案例集锦
139 0
|
消息中间件 JavaScript 小程序
如何将 @Transactional 事务注解运用到炉火纯青?
如何将 @Transactional 事务注解运用到炉火纯青?
|
SQL 消息中间件 JavaScript
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?
我在项目里用@Transactional注解控制事务,结果完全不生效,纳尼?