MySQL 中如果发生死锁应该如何解决?
- 标签:后端、MySQL、数据库
🏛 业务架构师
死锁的处理我分事前预防、事中恢复、事后治理三段讲,这是线上真实的工作流:
事前预防(成本最低)
- 统一加锁顺序:经典案例是转账,A 转 B 与 B 转 A 同时发生,互相持有对方要的锁。统一按账户 ID 排序加锁,环路就不存在了——用业务规则消除循环等待条件;
- 事务尽量小:锁持有时间短,交叉窗口就小;事务内不做 RPC、不发消息;
- 更新必须走索引:否则行锁退化为锁全表扫过的所有行,死锁概率暴涨;
- 降低隔离级别到 RC(减少间隙锁),高并发写入下死锁率明显下降。
事中恢复
- InnoDB 会自动死锁检测(innodb_deadlock_detect)回滚代价小的事务,应用要捕获死锁异常并有限重试——这是业务代码必须有的韧性;
- 关闭检测靠锁等待超时兜底的方案只适合死锁极罕见的系统。
事后治理
SHOW ENGINE INNODB STATUS拿最近一次死锁的 LATEST DETECTED DEADLOCK,定位两支事务持有什么等什么;- 死锁监控纳入告警,死锁率突增往往是业务变更引入的(新查询路径、批量任务改时段),早发现早治。
架构视角总结:死锁不是纯技术问题,加锁顺序本质是业务协议——协议要在设计评审时定下来,而不是靠线上反复救火。
🏗 系统架构师
死锁处理我按"定位 → 解除 → 预防"三步走:
定位:show engine innodb status 的 LATEST DETECTED DEADLOCK 段直接给出两个事务持有的锁和执行的 SQL;开 innodb_print_all_deadlocks 把所有死锁写进错误日志做长期统计,找到高频死锁点。
解除:InnoDB 自动死锁检测(wait-for graph)会回滚代价小的事务,业务侧要对死锁异常(1213)做捕获重试——这是应用层必须兜的。
预防(重点):
- 统一加锁顺序:批量更新按主键排序后依次执行,两个事务以相同顺序拿锁就不会成环,这是我治理死锁的第一原则;
- 缩小事务:事务越短持锁越短,交叉概率越低;事务内去掉 RPC、sleep、慢查询;
- 降幅:RR 降 RC 消除间隙锁;更新走索引避免锁扩大到全表记录;
- 大冲突场景改乐观锁或队列串行化,从模型上消除锁竞争。
死锁四条件的"循环等待"是最容易被工程手段打破的一环,统一顺序就是打破它的标准做法。
💻 全栈开发专家
先应急,再根治。
应急三步:
- 看死锁现场:
SHOW ENGINE INNODB STATUS,拉到LATEST DETECTED DEADLOCK段,能看到两个事务各自持有什么锁、等什么锁、执行的最后 SQL。 - 确认影响:InnoDB 会自动检测并回滚代价小的事务(undo 量小的),所以一般不会永久卡死,业务层要有"死锁重试"(捕获 1213 死锁错误,随机 sleep 后重试 2~3 次)。
- 紧急时按
sys.innodb_lock_waits给出的连接 kill 阻塞源头。
根治手段(按我的实战频率排):
- 统一加锁顺序:批量更新按主键排序后再逐条更新,两个事务就不会交叉持锁等待。
- 缩小事务:把 RPC、文件操作挪出事务,锁持有时间从秒级降到毫秒级,死锁概率指数下降。
- 走索引:无索引的 update 条件会锁住扫描到的所有行,锁范围暴涨。
- 降低隔离级别 RR → RC,减少间隙锁。
- 热点行改为队列串行更新(比如扣减先入 Redis 队列)。
预防:压测场景加死锁监控(performance_schema.events_errors_summary_global_by_error 里 MySQL ER_LOCK_DEADLOCK 计数)。