真正要盯的是mysql内部的锁等待链,通过show engine innodb status\g查看waiting for this lock to be granted和holds the lock(s)两段,结合innodb_print_all_deadlocks=on、explain分析索引使用、information_schema.innodb_trx定位长期持锁事务,并避免在事务中混入外部调用。

查 SHOW ENGINE INNODB STATUS 看真实锁关系
死锁报错 Deadlock found when trying to get lock; try restarting transaction 只是表象,真正要盯的是 MySQL 内部的锁等待链。执行 SHOW ENGINE INNODB STATUS\G 后重点看两段:WAITING FOR THIS LOCK TO BE GRANTED(谁在等什么锁)、HOLDS THE LOCK(S)(谁持有哪把锁)。这两段会明确写出事务 ID、SQL 语句、锁类型(如 X 或 S)、被锁的索引名和具体记录(如 PRIMARY, id=105),比应用日志可靠得多。
必须提前开启 innodb_print_all_deadlocks = ON(写进 my.cnf 并重启),否则只保留最后一次死锁快照,压测时大量死锁会被丢弃。
用 EXPLAIN 确认 JOIN 实际驱动表和索引使用
别信 SQL 里写的表顺序——MySQL 优化器可能重排。执行 EXPLAIN FORMAT=TRADITIONAL 或 EXPLAIN FORMAT=TREE(MySQL 8.0+),看 type 字段是否为 ref 或 range;如果是 ALL 或 index,说明驱动表全表扫描,会连带对被驱动表做大量随机加锁,死锁概率陡增。
检查 key 字段是否非空,确认是否真用了索引;rows 值过大(比如几万)也意味着锁范围失控。常见陷阱:ON CAST(o.user_id AS CHAR) = u.uid 这类隐式转换会让两边索引全部失效。
- 驱动表的
WHERE条件列必须有索引(最好联合索引覆盖WHERE + JOIN字段) - 被驱动表的
ON列必须是索引键(主键或二级索引前缀) - 避免在
JOIN条件里用函数、类型不一致(如INT关联VARCHAR)
定位长期持锁事务:查 INFORMATION_SCHEMA.INNODB_TRX
死锁常由“慢事务”引发——不是 SQL 慢,而是事务里夹了 HTTP 调用、Redis 读写或循环逻辑,导致锁持有时间从毫秒级拉长到秒级。这时哪怕加锁顺序一致,也容易触发 Lock wait timeout exceeded 或加剧死锁频率。
执行 SELECT trx_id, trx_state, trx_started, trx_wait_started, trx_mysql_thread_id, trx_query FROM INFORMATION_SCHEMA.INNODB_TRX,重点关注 trx_state = 'RUNNING' 且 trx_started 时间远早于当前时间的事务。它们很可能就是长期持锁的源头。
注意:trx_wait_started 不为空,说明该事务已在等待锁;若 trx_wait_started 与 trx_started 接近,大概率是刚卡住;若间隔数秒以上,基本可断定外部依赖拖慢了事务。
避免靠重试掩盖问题
应用层加死锁重试(捕获错误码 1213)看似简单,但会放大问题:重试让相同 SQL 更高频地争抢同一组资源,反而提高死锁率;尤其当事务本身含外部调用时,重试只是把失败延迟了几百毫秒,没解决根本瓶颈。
真正要做的,是把非 DB 操作(HTTP、缓存、日志)全部移出 BEGIN…COMMIT 块;所有涉及 UPDATE JOIN 的场景,优先拆成 SELECT id ORDER BY id + UPDATE … WHERE id IN (…),ID 列表升序排列,彻底消除跨表加锁顺序不确定性。
最容易被忽略的点:视图或 ORM 自动生成的逆向关联(如 Django 的 user.order_set.all())会隐藏真实 JOIN 顺序,必须拆开看生成的 SQL,再用 EXPLAIN 验证执行计划。











