你好呀,我是why。
前几天在某平台看到一个技术问题,很有意思啊。
涉及到的两个技术点,大家平时开发使用的也比较多,但是属于一个小细节,深挖下去,还是有点意思的。
来,先带你看一下问题是什么,同时给你解读一下这个问题:
https://segmentfault.com/q/1010000040361592
首先,这位同学给出了一个代码片段:
他说他有一个 func 方法,这个方法里面干了两件事:
1.先查询数据库里面的商品库存。
2.如果还有库存,那么对库存进行减一操作,模拟商品卖出。
对于第二件事,提问的同学其实写了两个操作在里面,所以我再细分一下:
2.1 对库存进行减一操作。
2.2 在订单表插入订单数据。
很显然,这两个操作都会对数据库进行操作,且应该是应该原子性的操作。
所以,在方法上加了一个 @Transactional 注解。
接着,为了解决并发访问的问题,他用 lock 把整个代码包裹了起来,保证在单体结构下,同一时刻只有一个请求能去执行减少库存,生成订单的操作。
非常的完美。
首先,先把大前提申明一下:MySQL 数据库的隔离机制使用的是可重复读级别。
这个时候,问题就来了。
如果是高并发的情况下,假设真的就有多个线程同时调用 func 方法。
要保证一定不能出现超卖的情况,那么就需要事务的开启与提交能完整的包裹在 lock 与 unlock之间。
显然事务的开启一定是在 lock 之后的。
故关键在于事务的提交是否一定在 unlock 之前?
如果事务的提交在 unlock 之前,没有问题。
因为事务已经提交了,代表库存一定减下来了,而这个时候锁还没释放,所以,其他线程也进不来。
画个简单的示意图如下:
等 unlock 之后,再进来一个线程,执行查询数据库的操作,那么查询到的值一定是减去库存之后的值。
但是,如果事务的提交是在 unlock 之后,那么有意思的事情就出现了,你很有可能发生超卖的情况。
上面的图就变成了这样的了,注意最后两个步骤调换了:
举个例子。
假设现在库存就只有一个了。
这个时候 A,B 两个线程来请求下单。
A 请求先拿到锁,然后查询出库存为一,可以下单,走了下单流程,把库存减为 0 了。
但是由于 A 先执行了 unlock 操作,释放了锁。
B 线程看到后马上就冲过来拿到了锁,并执行了查询库存的操作。
注意了,这个时候 A 线程还没来得及提交事务,所以 B 读取到的库存还是 1,如果程序没有做好控制,也走了下单流程。
哦豁,超卖了。
所以,再次重申问题:
在上面的示例代码的情况下,如果事务的提交在 unlock 之前,是没有问题的。但是如果在 unlock 之后是会有问题的。
那么事务的提交到底是在 unlock 之前还是之后呢?
这个事情,先把问题听懂了,接着我们先按下不表。你可以简单的思考一下。
我想先聊聊这句被我轻描淡写,一笔带过,你大概率没有注意到的话:
显然事务的开启一定是在 lock 之后的。
这句话,不是我说的,是提问的同学说的:
你有没有一丝丝疑问?
怎么就显然了?哪里就显然了?为什么不是一进入方法就开启事务了?
请给我证据。
来吧,瞅一眼证据。
事务开启时机证据,我们需要去源码里面找。
另外,我不得不多说一句 Spring 在事务这块的源码写的非常的清晰易懂,看起来基本上没有什么障碍。
所以如果你不知道怎么去啃源码,那么事务这块源码,也许是你撕开源码的一个口子。
好了,不多说了,去找答案。
答案就藏在这个方法里面的: