两个事务同时改同一条数据,一个超时失败了——怎么排查死锁?
先说结论
死锁是两个事务互相等待对方持有的锁,谁都不能继续。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年大数据老兵,聊技术、聊职场、聊成长