订阅更新

两个事务同时改同一条数据,一个超时失败了——怎么排查死锁?

MySQL手册 第 3 题 约 4 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:虚拟礼物交易死锁
  3. 排查步骤
  4. 解决方案:统一加锁顺序
  5. 其他常见的避免死锁的方法
  6. 总结

先说结论

死锁是两个事务互相等待对方持有的锁,谁都不能继续。MySQL的InnoDB会自动检测死锁并回滚其中一个事务。排查死锁的核心是看SHOW ENGINE INNODB STATUS 和 information_schema.INNODB_TRX。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

死锁是MySQL生产环境里最让人头疼的问题之一。它不像慢查询那么容易发现——慢查询是“慢但能跑完”,死锁是“直接报错”。我在元宇宙社交App做虚拟礼物交易时就遇到过死锁,今天把排查和解决的完整过程讲清楚。

业务场景:虚拟礼物交易死锁

我们的App有“送礼物”功能,涉及两步:扣送礼人的币、加收礼人的币。两个用户同时互相送礼时,发生了死锁:

-- 事务A:用户X给用户Y送礼
BEGIN;
UPDATE wallet SET balance = balance - 100 WHERE user_id = 'X';  -- 锁住用户X
UPDATE wallet SET balance = balance + 100 WHERE user_id = 'Y';  -- 等待用户Y的锁
 
-- 事务B:用户Y给用户X送礼(同时发生)
BEGIN;
UPDATE wallet SET balance = balance - 50 WHERE user_id = 'Y';   -- 锁住用户Y
UPDATE wallet SET balance = balance + 50 WHERE user_id = 'X';   -- 等待用户X的锁
 
-- 死锁!事务A等B释放Y,事务B等A释放X

排查步骤

第一步:查看死锁日志

SHOW ENGINE INNODB STATUS\G
-- 找到 LATEST DETECTED DEADLOCK 部分
-- 会显示两个事务分别持有和等待的锁

第二步:查看当前锁等待

-- 查看正在运行的事务
SELECT * FROM information_schema.INNODB_TRX;
 
-- 查看锁等待关系
SELECT * FROM information_schema.INNODB_LOCK_WAITS;  -- MySQL 5.7
SELECT * FROM performance_schema.data_lock_waits;     -- MySQL 8.0

解决方案:统一加锁顺序

死锁的根本原因是两个事务以不同的顺序加锁。解决方案很简单:统一加锁顺序。

-- ✔ 正确做法:总是先锁user_id小的,再锁user_id大的
-- 事务A:X给Y送礼(X < Y)
BEGIN;
UPDATE wallet SET balance = balance - 100 WHERE user_id = 'X';  -- 先锁小的
UPDATE wallet SET balance = balance + 100 WHERE user_id = 'Y';  -- 再锁大的
COMMIT;
 
-- 事务B:Y给X送礼(X < Y,所以仍然先锁X)
BEGIN;
UPDATE wallet SET balance = balance + 50 WHERE user_id = 'X';   -- 先锁小的
UPDATE wallet SET balance = balance - 50 WHERE user_id = 'Y';   -- 再锁大的
COMMIT;

这样两个事务的加锁顺序一致(都是先X后Y),就不会死锁了。

其他常见的避免死锁的方法

① 减小事务粒度: 事务里只做必要的操作,尽快提交。不要在事务里做复杂的业务逻辑或RPC调用。

② 加索引减少锁范围: 没有索引的UPDATE会锁全表,有索引只锁特定行。

③ 设置合理的锁等待超时: innodb_lock_wait_timeout默认50秒,可以调短到几秒,快速失败快速重试。

总结

死锁排查看SHOW ENGINE INNODB STATUS,预防靠统一加锁顺序。面试时能用一个真实场景(像转账或送礼)把死锁的原因和解决方案说清楚,就很稳。

下一篇聊一个非常实用的场景:主从延迟导致读到过期数据,你怎么解决?

你们生产环境遇到过死锁吗?怎么解决的?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器