单表更新死锁主因是加锁顺序不一致、索引失效或间隙锁干扰,解决核心是统一in列表顺序、确保索引有效、避免范围查询,并在应用层捕获1213错误重试。

单表更新死锁不是“锁太紧”导致的,而是事务对同一张表里多行加锁时顺序不一致、锁范围失控或索引失效引发的交叉等待。解决核心是让锁行为可预测、可收敛。
为什么 UPDATE WHERE id IN (5,1,3) 会高频死锁
MySQL 的 UPDATE 按 SQL 中 WHERE 条件出现的顺序加锁,而不是按主键物理顺序。如果两个事务分别执行 UPDATE t SET x=1 WHERE id IN (5,1,3) 和 UPDATE t SET x=1 WHERE id IN (3,5,1),InnoDB 可能先锁 5→1→3 和 3→5→1,形成 ABBA 循环。
- 必须强制统一条件顺序:所有批量更新前对 ID 列做升序排序,再拼成
WHERE id IN (1,3,5) - ORM 动态生成 SQL 时容易打乱顺序(如 MyBatis 的
<if></if>分支),建议关键事务禁用动态条件,改用预编译 + 排序后参数绑定 - 不要依赖数据库自动优化顺序——InnoDB 不会对
IN列表重排,EXPLAIN也看不出加锁顺序
无索引字段更新等于主动制造表级锁压力
当 UPDATE t SET status='done' WHERE biz_no = 'xxx' 中 biz_no 没有索引,InnoDB 在 REPEATABLE READ 下会全表扫描并为每一行加 X locks rec but not gap,还可能对间隙加锁。此时哪怕只更新 1 行,实际锁了上千行,和其他事务冲突概率陡增。
- 用
EXPLAIN验证每条更新语句是否走了索引;注意隐式类型转换(如WHERE biz_no = 123但字段是VARCHAR)会让索引失效 - 复合条件优先建联合索引,例如
WHERE biz_no = ? AND status = ?→ 建(biz_no, status),而非单独索引 - 避免在索引列上用函数:
WHERE DATE(create_time) = '2026-09-28'无法走create_time索引
间隙锁(Gap Lock)在单表场景下怎么偷偷搞事情
REPEATABLE READ 是 MySQL 默认隔离级别,它让范围查询(包括 WHERE status IN ('pending','processing') 这种等值列表)也可能触发间隙锁。如果两个事务都执行类似 SELECT ... FOR UPDATE WHERE created_at > '2026-09-20',即使查到的行不重叠,间隙重叠也会互相阻塞。
- 确认业务是否真需要 RR:若能接受幻读,直接设
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,InnoDB 会禁用间隙锁 - 单表高频更新尽量避免范围条件,改用主键 or 唯一键精确匹配;实在要范围操作,考虑拆成多次小批量 + 主键升序
-
innodb_lock_wait_timeout调大不能解决死锁,只会让等待更久;重点应设innodb_deadlock_detect = ON(默认已开)保证快速回滚
应用层必须做且只能靠自己做的三件事
数据库不会替你重试,也不会告诉你哪次失败该补发消息。死锁错误码 1213(Deadlock found when trying to get lock)必须由代码捕获并处理。
- 所有写操作外层包重试逻辑,指数退避(如 10ms → 30ms → 100ms),最多 3 次;超过则抛业务异常,不可静默吞掉
- 事务内禁止调用外部 HTTP/API 或 sleep,否则锁持有时间不可控,把“秒级死锁”拖成“分钟级阻塞”
- 开启
innodb_print_all_deadlocks = ON,把死锁日志接入 ELK 或 Loki,按transaction id关联应用 trace,才能定位到底是哪个接口、哪个用户 ID 触发的
最易被忽略的一点:死锁日志里显示的 SQL 往往只是“最后一根稻草”,真正的问题常藏在它之前那几条没报错的 SELECT ... FOR UPDATE 或长事务里——别只盯着报错那条语句看。











