MySQL 死锁不报 1213:先确认 innodb_print_all_deadlocks 有没有开
线上死锁很少老老实实抛 Deadlock found when trying to get lock。更常见的表现是「连接超时」「应用卡顿」「系统繁忙,请稍后再试」,排查方向一下被带到网络和连接池上。
原因是死锁发生时,两个(或多个)事务互相等待对方持有的锁。等待时间超过 innodb_lock_wait_timeout(默认 50 秒)后,MySQL 强制回滚其中一个事务,对外抛出的往往是超时类异常,而不是 1213。于是真正的问题被掩盖。
参考来源:MySQL 线上死锁排查、MySQL 死锁排查指南
定位手段
1. 错误日志
前提是开了 innodb_print_all_deadlocks=1。没开的话,错误日志里没有死锁记录,SHOW ENGINE INNODB STATUS 里也只保留最近一次。
2. SHOW ENGINE INNODB STATUS
SHOW ENGINE INNODB STATUS\G
看 LATEST DETECTED DEADLOCK 段,里面有两个事务的 SQL、加锁顺序,以及最终被回滚的是哪个事务。
3. 累计统计
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';
用这个值评估死锁的频发程度,判断是个例还是持续发生。
4. 锁等待现场
旧版本看 information_schema 的 INNODB_LOCKS / INNODB_LOCK_WAITS。新版 MySQL 已经迁移到 performance_schema,对应 data_locks / data_lock_waits:
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
5. 慢查询联动
写入类慢 SQL 会大幅拉长锁持有时间,把毫秒级的锁占用拉到秒级,引发连锁锁等待。有 pt-query-digest 就分析慢日志 TOP;没有的话直接查 performance_schema:
SELECT *
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
6. 慢查询与未走索引语句
开启慢查询日志,并记录未走索引的语句(log_queries_not_using_indexes),把「慢」和「锁」两条线并起来看。
常见根因类型
- 加锁顺序不一致(最典型):事务 A 先锁
id=1再锁id=2,事务 B 先锁id=2再锁id=1,形成等待环。 - 热点数据更新:多个事务同时更新同一行,例如积分表
user_points,锁冲突集中在一行上。 - 无索引更新导致锁范围扩大:
order_item表的order_id没有索引,一条UPDATE触发全表扫描,锁住大量行。 - 外键约束带来的额外锁:外键检查会在父表/子表上加锁,容易在没意识到的地方形成等待。
- 事务里做外部调用:事务中调用外部接口、
sleep、写日志、发消息,把锁窗口拉长,形成跨系统的隐式死锁。
参数取舍
innodb_lock_wait_timeout
默认 50 秒。在网关超时 3 秒的场景下,50 秒明显过长——请求早就超时了,事务还挂在锁上。不少团队调到 5 秒或 3 秒。但也不能太小,否则会误杀正常的锁等待。建议结合接口 P99 来设定。
innodb_deadlock_detect
默认 ON,一般不要关。关掉之后死锁退化成普通锁等待,由 innodb_lock_wait_timeout 兜底:原本几毫秒就能返回 1213 的错误,变成几十秒才超时,连接池更容易被打垮。
有个案例:某项目组为了提高吞吐关掉了死锁检测,结果请求堆积把连接池拖垮,恢复花了两小时。
预防
- 统一加锁顺序:所有事务按
id升序加锁;或者用一次性拿锁的方式:
SELECT ... WHERE id IN (1,2) FOR UPDATE;
- 缩小事务粒度:单事务从
begin到commit控制在 100ms 内比较健康,超过 1 秒要重点查。 - 事务内禁止远程调用:不在事务里
sleep、发消息、调外部接口。 - 无索引更新先补索引:减少锁范围,避免全表扫描级别的加锁。
- 打开 innodb_print_all_deadlocks:长期统计死锁,而不是等出事时才发现日志里什么都没有。