死锁的产生与应对思路
InnoDB 中两个及以上事务互相持有对方需要的锁、又不肯释放,就形成死锁,数据库检测到后会回滚代价较小的一方并报 1213 错误。工程上更现实的目标是减少发生,而不是彻底消除:统一加锁顺序、缩小事务范围、把热点行拆散,再配合应用层的有限次退避重试,就能把死锁从事故变成可预期的小概率事件。
基本信息
| 适用平台 | InnoDB(MySQL 5.7 / 8.0) |
|---|---|
| 错误码 | 1213 Deadlock found when trying to get lock |
| 开启日志 | innodb_print_all_deadlocks = ON |
| 诊断入口 | SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK |
| 重试原则 | 有限次数 + 随机退避,避免重试风暴 |
典型使用场景
- 转账、扣减库存等成对更新两张表或两行数据的业务
- 批量任务与在线事务同时改同一批记录
- 订单状态流转:多状态字段被不同流程交叉更新
- 唯一键冲突与插入意向锁交织导致的死锁排查
- 长事务持有锁期间被短事务反复碰撞
定序加锁示例
定序加锁示例
-- 反例:两个事务按不同顺序更新两行,极易互等
-- 正例:统一按 id 升序加锁,破坏循环等待
BEGIN;
SELECT * FROM appdb.accounts WHERE id = 1 FOR UPDATE;
SELECT * FROM appdb.accounts WHERE id = 2 FOR UPDATE;
UPDATE appdb.accounts SET balance = balance - 100 WHERE id = 1;
UPDATE appdb.accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;定位最近一次死锁
定位最近一次死锁
-- 打开死锁日志记录(8.0 支持动态开启)
SET GLOBAL innodb_print_all_deadlocks = ON;
-- 查看最近一次死锁的两个事务与各自持有的锁
SHOW ENGINE INNODB STATUS\G
-- 8.0 可从 performance_schema 查看当前等待关系
SELECT * FROM sys.innodb_lock_waits\G
-- 查看未提交事务(长事务是死锁的放大器)
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx;常见死锁模式
| 模式 | 成因 | 规避 |
|---|---|---|
| 加锁顺序不一致 | 两事务更新同两行的顺序相反 | 统一按主键升序处理 |
| 间隙锁互等 | 范围查询加临键锁后交叉更新 | 缩小范围条件或改用等值匹配 |
| 唯一键冲突 | 并发插入同一唯一键 | 先查后插、捕获重复键错误重试 |
| 大事务持锁时间过长 | 事务内混入 RPC、循环更新 | 拆小事务,先计算后写库 |
| 热点行集中更新 | 计数器、库存单行被高频更新 | 分桶或改异步合并写 |
出现死锁说明两个事务在锁顺序上有冲突,这是业务模型问题不是数据库故障:先定位是哪两条 SQL 交叉,再决定是调顺序、拆事务还是改数据模型。
重试必须有上限和退避:死锁风暴期间无限制重试会把数据库压力放大数倍,形成自我加剧的恶性循环。