org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
不信?你看嘛,我不骗你。
它们之间只隔了三个调用:
这样就找到答案了。
调用栈,另一个调试源码小技巧,屡试不爽,送给你。
之前还是之后好了,前面是开胃菜,可能有的同学吃开胃菜就已经弄饱了。
没事,现在上正餐,再按一按还是能吃进去的。
还是拿前面的这份代码来说事,流程就是这样的:
1.先拿锁。
2.查询库存。
3.判断是否还有库存。
4.有库存则执行减库存,创建订单的逻辑。
5.没有库存则返回。
6.释放锁。
所以代码是这样的:
完全符合我们之前的那份代码片段,有事务,也有锁:
回到我们最开始抛出来的问题:
在上面的示例代码的情况下那么事务的提交到底是在 unlock 之前还是之后呢?
我们可以带入一个具体的场景。
比如我数据库里面有 10 个顶配版的 iPad,原价 1.6w 元一台,现在单价 1w 一个,这个价格够秒杀吧?
反正一共就 10 台,所以,我的数据库里面是这样的,
然后我搞 100 个人来抢东西,不过分吧?
我这里用 CountDownLatch 来模拟一下并发:
执行一下,先看结果,立马就见分晓:
动图右边的部分:
上面是浏览器请求,触发 Controller 的代码。
然后中间是产品表,有 10 个库存。
最下面是订单表,没有一条数据。
触发了代码之后,库存为 0 了,没有问题。
但是,订单居然有 20 笔!
也就是说超卖了 10 个ipad pro 顶配版!
超卖的,可不在活动预算范围内啊!
那可就是一个 1.6w 啊,10 个就是 16w 啊。
就这么其貌不扬,人畜无害,甚至看起来猥猥琐琐的代码,居然让我亏了整整 16w 。
其实,结果出现了,答案也就随之而来了。
在上面的示例代码的情况下,事务的提交在 unlock 之后。
其实你仔细分析后,猜也能猜出来,肯定是在 unlock 之后的。
而且上面的描述“unlock之后”其实是有一定的迷惑性的,因为释放锁是一个比较特别的操作。
换一个描述,就比较好理解了:
在上面的示例代码的情况下,事务的提交在方法运行结束之后。
你细品,这个描述是不是迷惑性就没有那么强了,甚至你还会恍然大悟:这不是常识吗?
为什么是方法结束之后,分析具体原因之前,我想先简单分析一下这样的代码写出来的原因。
我猜可能是这样的。
最开始的代码结构是这样:
然后,写着写着发现不对,并发的场景下,库存是一个共享的资源,这玩意得加锁啊。
于是搞了这出:
后面再次审查代码的时候,发现:哟,这个第三步得是一个事务操作才行呀。
于是代码就成了这样:
演进路线非常合理,最终的代码看起来也简直毫无破绽。
但是问题到底出在哪里了呢?
找答案答案还是在这个类里面: