定位死锁须执行show engine innodb status\g,重点分析latest detected deadlock中“waiting for this lock to be granted”与“holds the lock(s)”两段,精准匹配事务id、表、索引、行值,确认循环等待链;同时开启innodb_print_all_deadlocks=on以防快照覆盖。

死锁不是JOIN写错了,而是多个事务对同一组行加锁的顺序不一致——定位必须看InnoDB内部锁等待链,修复核心是统一驱动表、补索引、拆事务。
怎么看真实锁等待链(别只信错误日志)
报错Deadlock found when trying to get lock; try restarting transaction只是数据库杀掉牺牲者的提示,真正线索藏在SHOW ENGINE INNODB STATUS\G里。执行后重点盯两段:
-
WAITING FOR THIS LOCK TO BE GRANTED:谁在等什么锁?会写出事务ID、SQL、锁类型(X/S)、具体记录(如PRIMARY, id=105) -
HOLDS THE LOCK(S):谁持有什么锁?同样带表名、索引名、行值
这两段能直接对应上“事务A锁着orders.id=105,等着users.id=202;事务B锁着users.id=202,等着orders.id=105”——这才是循环等待的铁证。务必提前开启innodb_print_all_deadlocks = ON,否则压测时大量死锁快照会被覆盖。
为什么EXPLAIN比SQL写法更关键
MySQL优化器可能重排表顺序,你写的UPDATE orders JOIN users,实际执行可能是先锁users再锁orders。用EXPLAIN FORMAT=TRADITIONAL看:
-
type字段是ALL或index?说明驱动表全表扫描,会连带对被驱动表做大量随机加锁 -
key为空或rows极大(几万)?说明JOIN条件没走索引,锁范围失控 - 出现
Using temporary; Using filesort?这类操作常伴随宽锁范围
特别警惕隐式转换:ON CAST(o.user_id AS CHAR) = u.uid会让两边索引全部失效,优化器被迫全表扫。
怎么让锁顺序稳定可预测
靠重试掩盖不了问题,得从执行计划源头控制:
- 用
STRAIGHT_JOIN强制驱动表顺序,比如SELECT STRAIGHT_JOIN * FROM orders JOIN users ON ...确保永远从orders开始遍历 - 驱动表的
WHERE列 + 被驱动表的ON列,都必须有索引;更优是联合索引覆盖过滤+关联字段(如(created_at, user_id)) - 避免在JOIN条件里用函数、类型不一致(如
INT关联VARCHAR),否则索引失效 - 大表关联小表时,显式用小表做驱动表(数据少、锁行少)
视图里的JOIN尤其危险——它隐藏了真实执行顺序,调用侧最好绕过视图,手写STRAIGHT_JOIN语句并和视图内部顺序保持一致(查EXPLAIN确认)。
哪些操作会悄悄延长锁持有时间
死锁常由“慢事务”引发,不是SQL慢,而是事务里夹了外部依赖:
- 查
INFORMATION_SCHEMA.INNODB_TRX,重点关注trx_state = 'RUNNING'且trx_started远早于当前时间的事务——它们很可能卡在HTTP调用、Redis读写或循环里 -
trx_wait_started不为空且与trx_started间隔数秒以上?基本可断定外部拖慢了事务 - 事务里混用
SELECT FOR UPDATE和后续UPDATE,尤其当SELECT没ORDER BY时,加锁顺序随机,极易冲突
真正容易被忽略的是:即使你补了所有索引、调了隔离级别,只要事务里还藏着一次未超时的API调用,锁就一直挂着——死锁窗口就始终存在。










