死锁是多个事务互相等待对方持有的锁而形成循环依赖,如事务a先锁id=1再等id=2,事务b反之;innodb在可重复读级别下主动检测并回滚其一,报错1213。

死锁是怎么发生的(以 UPDATE 为例)
MySQL 的死锁不是“卡住”,而是两个或多个事务互相等待对方持有的锁,形成循环依赖。最常见于并发执行 UPDATE 时按不同顺序访问同一组行。比如事务 A 先锁 id=1 再试图锁 id=2,而事务 B 反过来先锁 id=2 再等 id=1——InnoDB 检测到后会主动回滚其中一个事务,并报错:Deadlock found when trying to get lock; try restarting transaction
- 死锁只在 可重复读(REPEATABLE READ) 隔离级别下会被 InnoDB 主动检测并中断
-
SELECT ... FOR UPDATE 和 UPDATE 在有索引条件下加的是行级锁;若走全表扫描,可能升级为表锁,大幅增加死锁概率
- 没有索引的
WHERE 条件会让 MySQL 无法精确定位行,从而对更多无关行加锁,埋下隐患
如何复现和确认死锁日志
死锁发生后,MySQL 不会静默失败,但也不会自动重试。你需要主动查 INFORMATION_SCHEMA.INNODB_TRX 和错误日志,或者开启死锁检测日志:
- 执行
SHOW ENGINE INNODB STATUS\G
,在输出末尾的 LATEST DETECTED DEADLOCK 区域能看到最近一次死锁的完整参与者、SQL、持锁/等锁状态
- 确保
innodb_print_all_deadlocks = ON(写入 error log),避免只靠 SHOW ENGINE 查历史
- 注意:该日志不记录事务开始时间或应用层上下文,需结合业务日志定位具体代码路径
避免死锁的四个实操原则
核心思路是「消除加锁顺序不确定性」,而非单纯减少事务长度:
- 所有涉及多行更新的逻辑,固定行处理顺序:例如统一按
ORDER BY id ASC 排序后再更新,避免 A 事务按 id 升序、B 事务按降序操作同一数据集
- 尽量在事务内 一次性申请所有需要的锁:把多个
UPDATE 合并在一条语句中(如 UPDATE t SET x=1 WHERE id IN (1,2,3)),而不是分多次单行更新
- 避免在事务中混用
SELECT ... FOR UPDATE 和普通 SELECT:前者加锁,后者不加,容易导致后续 UPDATE 因条件变化触发额外锁竞争
- 更新语句必须走索引:检查
EXPLAIN 输出中的 key 字段是否非 NULL;若显示 NULL,说明走了全表扫描,应补上对应索引
应用层该怎么安全重试
MySQL 报 Deadlock found when trying to get lock 后,事务已回滚,应用必须捕获该错误并重试——但不能无脑重试:
- 只对明确知道是死锁的错误码重试:
1213(MySQL errno),其他错误如 1205(超时)或主键冲突不应重试
- 设置最大重试次数(通常 ≤ 3),防止雪崩;每次重试前加随机微小延迟(如 10–100ms),错开竞争窗口
- 重试时要保证业务幂等:例如用
INSERT ... ON DUPLICATE KEY UPDATE 替代先查后插,或用唯一业务字段做插入校验
- 不要在存储过程中封装重试逻辑:MySQL 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
SELECT ... FOR UPDATE 和 UPDATE 在有索引条件下加的是行级锁;若走全表扫描,可能升级为表锁,大幅增加死锁概率WHERE 条件会让 MySQL 无法精确定位行,从而对更多无关行加锁,埋下隐患INFORMATION_SCHEMA.INNODB_TRX 和错误日志,或者开启死锁检测日志:
- 执行
SHOW ENGINE INNODB STATUS\G
,在输出末尾的LATEST DETECTED DEADLOCK区域能看到最近一次死锁的完整参与者、SQL、持锁/等锁状态 - 确保
innodb_print_all_deadlocks = ON(写入 error log),避免只靠SHOW ENGINE查历史 - 注意:该日志不记录事务开始时间或应用层上下文,需结合业务日志定位具体代码路径
避免死锁的四个实操原则
核心思路是「消除加锁顺序不确定性」,而非单纯减少事务长度:
- 所有涉及多行更新的逻辑,固定行处理顺序:例如统一按
ORDER BY id ASC 排序后再更新,避免 A 事务按 id 升序、B 事务按降序操作同一数据集
- 尽量在事务内 一次性申请所有需要的锁:把多个
UPDATE 合并在一条语句中(如 UPDATE t SET x=1 WHERE id IN (1,2,3)),而不是分多次单行更新
- 避免在事务中混用
SELECT ... FOR UPDATE 和普通 SELECT:前者加锁,后者不加,容易导致后续 UPDATE 因条件变化触发额外锁竞争
- 更新语句必须走索引:检查
EXPLAIN 输出中的 key 字段是否非 NULL;若显示 NULL,说明走了全表扫描,应补上对应索引
应用层该怎么安全重试
MySQL 报 Deadlock found when trying to get lock 后,事务已回滚,应用必须捕获该错误并重试——但不能无脑重试:
- 只对明确知道是死锁的错误码重试:
1213(MySQL errno),其他错误如 1205(超时)或主键冲突不应重试
- 设置最大重试次数(通常 ≤ 3),防止雪崩;每次重试前加随机微小延迟(如 10–100ms),错开竞争窗口
- 重试时要保证业务幂等:例如用
INSERT ... ON DUPLICATE KEY UPDATE 替代先查后插,或用唯一业务字段做插入校验
- 不要在存储过程中封装重试逻辑:MySQL 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
ORDER BY id ASC 排序后再更新,避免 A 事务按 id 升序、B 事务按降序操作同一数据集UPDATE 合并在一条语句中(如 UPDATE t SET x=1 WHERE id IN (1,2,3)),而不是分多次单行更新SELECT ... FOR UPDATE 和普通 SELECT:前者加锁,后者不加,容易导致后续 UPDATE 因条件变化触发额外锁竞争EXPLAIN 输出中的 key 字段是否非 NULL;若显示 NULL,说明走了全表扫描,应补上对应索引Deadlock found when trying to get lock 后,事务已回滚,应用必须捕获该错误并重试——但不能无脑重试:
- 只对明确知道是死锁的错误码重试:
1213(MySQL errno),其他错误如1205(超时)或主键冲突不应重试 - 设置最大重试次数(通常 ≤ 3),防止雪崩;每次重试前加随机微小延迟(如 10–100ms),错开竞争窗口
- 重试时要保证业务幂等:例如用
INSERT ... ON DUPLICATE KEY UPDATE替代先查后插,或用唯一业务字段做插入校验 - 不要在存储过程中封装重试逻辑:MySQL 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
死锁无法完全杜绝,但只要加锁顺序一致、索引到位、应用层有兜底重试,就能把影响控制在毫秒级瞬时失败范围内。最容易被忽略的是:开发时用单线程测试看不出问题,一上生产并发量上来,没排序的 IN 列表或缺失索引立刻暴露。










