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

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

不信?你看嘛,我不骗你。

它们之间只隔了三个调用:

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

这样就找到答案了。

调用栈,另一个调试源码小技巧,屡试不爽,送给你。

之前还是之后

好了,前面是开胃菜,可能有的同学吃开胃菜就已经弄饱了。

没事,现在上正餐,再按一按还是能吃进去的。

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

还是拿前面的这份代码来说事,流程就是这样的:

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

1.先拿锁。

2.查询库存。

3.判断是否还有库存。

4.有库存则执行减库存,创建订单的逻辑。

5.没有库存则返回。

6.释放锁。

所以代码是这样的:

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

完全符合我们之前的那份代码片段,有事务,也有锁:

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

回到我们最开始抛出来的问题:

在上面的示例代码的情况下那么事务的提交到底是在 unlock 之前还是之后呢?

我们可以带入一个具体的场景。

比如我数据库里面有 10 个顶配版的 iPad,原价 1.6w 元一台,现在单价 1w 一个,这个价格够秒杀吧?

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

反正一共就 10 台,所以,我的数据库里面是这样的,

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

然后我搞 100 个人来抢东西,不过分吧?

我这里用 CountDownLatch 来模拟一下并发:

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

执行一下,先看结果,立马就见分晓:

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

动图右边的部分:

上面是浏览器请求,触发 Controller 的代码。

然后中间是产品表,有 10 个库存。

最下面是订单表,没有一条数据。

触发了代码之后,库存为 0 了,没有问题。

但是,订单居然有 20 笔!

也就是说超卖了 10 个ipad pro 顶配版!

超卖的,可不在活动预算范围内啊!

那可就是一个 1.6w 啊,10 个就是 16w 啊。

就这么其貌不扬,人畜无害,甚至看起来猥猥琐琐的代码,居然让我亏了整整 16w 。

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

其实,结果出现了,答案也就随之而来了。

在上面的示例代码的情况下,事务的提交在 unlock 之后。

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

其实你仔细分析后,猜也能猜出来,肯定是在 unlock 之后的。

而且上面的描述“unlock之后”其实是有一定的迷惑性的,因为释放锁是一个比较特别的操作。

换一个描述,就比较好理解了:

在上面的示例代码的情况下,事务的提交在方法运行结束之后。

你细品,这个描述是不是迷惑性就没有那么强了,甚至你还会恍然大悟:这不是常识吗?

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

为什么是方法结束之后,分析具体原因之前,我想先简单分析一下这样的代码写出来的原因。

我猜可能是这样的。

最开始的代码结构是这样:

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

然后,写着写着发现不对,并发的场景下,库存是一个共享的资源,这玩意得加锁啊。

于是搞了这出:

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

后面再次审查代码的时候,发现:哟,这个第三步得是一个事务操作才行呀。

于是代码就成了这样:

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

演进路线非常合理,最终的代码看起来也简直毫无破绽。

但是问题到底出在哪里了呢?

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

找答案

答案还是在这个类里面:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzdfsx.html