库存扣减时,出现 MySQL 死锁是什么原因?

2023-11-28 10:59:21 +08:00
 jonsmith

在研究并发请求商品超售问题时,测试了几种 MySQL 的 sql 语句,其中一种情况产生死锁的问题很困惑,求大佬指点。

SQL 语句

UPDATE inventory SET stock = stock - Y WHERE product_id = X AND stock >= Y;

对比 SQL

// 事务中,先对库存加锁 for update ,避免其他事务修改库存
SELECT stock FROM inventory WHERE product_id = X FOR UPDATE;
// 再进行修改
UPDATE inventory SET stock = stock - Y WHERE product_id = X;

请教下第一种 sql 是什么原因造成的死锁?

3665 次点击
所在节点    MySQL
18 条回复
keymao
2023-11-28 11:06:44 +08:00
for update 会阻塞其他行级锁的请求,你上面那个 update 在库存不足的时候会执行失败,这样事务没执行完就锁着吧。

你不开事务的话,for update 也是不生效的。 所以也没有问题。
jonsmith
2023-11-28 11:12:58 +08:00
@keymao 两种 SQL 都在事务中执行的,当库存不足时,只有第 1 种会死锁。
我怀疑是不是 where 条件里 `stock >= Y` 在事务中会扩大锁的范围,产生一些奇怪的死锁逻辑。
jonsmith
2023-11-28 11:14:11 +08:00
又测试了第 3 种 SQL 语句:
```
UPDATE inventory SET stock = (case when (stock >= Y) then (stock - Y) else stock end) WHERE product_id = X;
```
这个在事务中也不会造成死锁。
jonsmith
2023-11-28 11:14:56 +08:00
MySQL 是默认 RR 事务级别
ezwd
2023-11-28 11:16:21 +08:00
MySQL 的 InnoDB 存储引擎用行级锁来实现多版本并发控制( MVCC ),同时也支持 "SELECT ... FOR UPDATE" 这种显式锁定。在解决超售问题的场景中,死锁的出现可能是由于并发事务试图在同一时间内锁定同一行数据导致的。

对于你的第一种 SQL 语句:

```
UPDATE inventory SET stock = stock - Y WHERE product_id = X AND stock >= Y;
```
这个语句在更新前会先检查 stock >= Y 条件,如果不满足这个条件,该行不会被锁定也不会被更新。当你有多个并发事务试图更新同一个 product_id ,并且 stock 库存不足时,这些事务可能会互相等待其他事务释放锁,这就可能导致死锁。

对于你的第二种 SQL 语句:

```
SELECT stock FROM inventory WHERE product_id = X FOR UPDATE;
UPDATE inventory SET stock = stock - Y WHERE product_id = X;
```
在这个情况下,你先显式地获取了一个行级锁,这会阻止其他事务在此期间修改这一行。然后,你再执行更新操作。由于你已经持有了行级锁,所以其他试图更新这一行的事务会被阻塞,直到你的事务完成,这样就避免了死锁。

总的来说,第一种 SQL 语句在高并发的情况下可能会导致死锁,因为它试图在同一时间内更新同一行。而第二种 SQL 语句通过显式获取行级锁来避免这个问题。这就是为什么第二种 SQL 语句在你的测试中没有出现死锁。
janwarlen
2023-11-28 11:35:23 +08:00
@ezwd #5 stock 库存不足就不满足 stock >= Y 的条件了啊,按照你说的不满足这个条件,该行不会被锁定也不会被更新,就不会死锁了...
jonsmith
2023-11-28 11:37:52 +08:00
@janwarlen 对,代码 bug ,抱歉,已经 append 了
asasjajsajsd
2023-11-28 11:49:26 +08:00
用加行锁的,防止脏数据
bololobo
2023-11-28 12:18:26 +08:00
这个案例体现了 innoDB 锁体系的一个特点 就是两阶段锁:在执行语句时加的锁,要到提交事务或者回滚事务的时候才释放
Pythoner666666
2023-11-28 12:25:27 +08:00
我们线上用的是第二种,就是为了避免死锁的情况
Oilybear
2023-11-28 13:39:36 +08:00
还有其他索引吗比如 stock 之类的
liprais
2023-11-28 14:02:17 +08:00
UPDATE inventory SET stock = stock - Y WHERE product_id = X AND stock >= Y
这种除非你还有个索引在 stock 上而且是倒序的而且还有另外一个 sql 去更新这个索引才会出现死锁
ZZ74
2023-11-28 14:24:43 +08:00
1 3 无差别...
2 会略微低一些 主要是并发性上。
另外 忘记 rollback.....话说还在手工控制事务么........
xiang0818
2023-11-28 16:04:00 +08:00
用了分布式事务吧
shenjinpeng
2023-11-28 16:13:54 +08:00
可以换别的实现 , 比如库存丢 redis , 然后用 redis 实现锁; 总感觉 MySQL 并发跟不上, 秒杀/营销 场景 容易超卖 ...
wu00
2023-11-28 16:58:12 +08:00
单看这 3 个语句:
1-乐观锁,stock >= Y 锁粒度最小化,性能最好,体验方面也比时间戳的 version 强得多;
2-悲观锁,碰到其它线程对这条数据进行 for update 时会进行排队,性能最低,但是优点是当你处理"一些业务逻辑"时不用担心当前事务并行执行;
3-没有存在的必要,似乎跟 1 没什么区别。
leorealman
364 天前
for update 简单说就是一致性锁定读,因为由于 MVCC 的存在每个事务都可以获取一个数据版本且可以修改它,所以有可能修改到和其他事务正在处理相同的某一行数据,不知道我解释清楚了没?
mmdsun
364 天前
@Pythoner666666 第一种应该不会死锁,楼主笔误写错了。


@shenjinpeng 性能差了点,但用 update + where 条件 不会超卖的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/995866

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX