死锁是怎么发生的(以 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 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
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 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
Deadlock found when trying to get lock后,事务已回滚,应用必须捕获该错误并重试——但不能无脑重试: 只对明确知道是死锁的错误码重试:
1213(MySQL errno),其他错误如
1205(超时)或主键冲突不应重试 设置最大重试次数(通常 ≤ 3),防止雪崩;每次重试前加随机微小延迟(如 10–100ms),错开竞争窗口 重试时要保证业务幂等:例如用
INSERT ... ON DUPLICATE KEY UPDATE替代先查后插,或用唯一业务字段做插入校验 不要在存储过程中封装重试逻辑:MySQL 存储过程无法捕获死锁异常并控制重试节奏,必须由应用层处理
死锁无法完全杜绝,但只要加锁顺序一致、索引到位、应用层有兜底重试,就能把影响控制在毫秒级瞬时失败范围内。最容易被忽略的是:开发时用单线程测试看不出问题,一上生产并发量上来,没排序的
IN列表或缺失索引立刻暴露。
