单行高频更新本身不会死锁,死锁必因多事务交叉操作多行或混入其他锁操作;关键在于锁类型、时机与范围冲突,而非单纯更新同一行。

单行高频更新本身不会死锁,死锁一定发生在多个事务交叉操作多行、或混入其他锁操作时。哪怕语句都是 UPDATE t SET x = ? WHERE id = 1,只要事务里夹杂了 SELECT ... FOR UPDATE、UPDATE 其他行、触发器、JOIN 子查询或未命中索引的范围扫描,就可能瞬间触发循环等待。
为什么“UPDATE 同一行”还会死锁?
表面看是单行,但 InnoDB 加锁行为取决于整个事务的执行路径:
- 事务 A:先
SELECT * FROM t WHERE status = 'pending' ORDER BY created_at LIMIT 1 FOR UPDATE(没索引 → 全表扫描加大量间隙锁),再UPDATE t SET status = 'processing' WHERE id = 123 - 事务 B:直接
UPDATE t SET status = 'done' WHERE id = 123 - 结果:A 持有间隙锁等 B 提交,B 持有 id=123 的记录锁等 A 释放间隙锁 → 死锁
关键点:不是“更新同一行”导致死锁,而是“同一行被不同锁类型、不同时机、不同范围的操作反复争夺”。
用乐观锁替代显式锁(推荐用于状态流转)
适用于订单状态变更、任务标记完成等幂等场景,绕过锁竞争,把冲突检测下推到 UPDATE 条件中:
UPDATE orders SET status = 'shipped', version = version + 1 WHERE id = 123 AND status = 'confirmed' AND version = 42;
如果返回影响行数为 0,说明已被其他事务更新,应用层捕获后重试即可。优势:
- 不加任何锁,彻底避开死锁链
- 失败可精确归因(status 不匹配 or version 不匹配)
- 比
SELECT ... FOR UPDATE+UPDATE减少一次网络往返和锁持有时间
注意:version 字段必须有索引,否则 WHERE 条件可能退化为全表扫描。
必须走索引,且避免间隙锁干扰
高频单行更新最常踩的坑是:以为 WHERE id = ? 就安全,却忽略了事务中其他语句是否命中索引。
- 所有
SELECT ... FOR UPDATE、UPDATE、DELETE的 WHERE 条件,都必须能走**唯一索引或主键**;用EXPLAIN FORMAT=TRADITIONAL验证type是const或eq_ref,且key显示具体索引名 - 避免在事务中使用非唯一条件(如
WHERE status = 'ready'),即使加了索引,在 RR 隔离级别下也会加 Next-Key Lock,扩大锁定范围 - 如果业务真要按状态选行,建联合索引如
(status, id),并强制ORDER BY id,再用LIMIT 1,确保每次只锁住一行物理记录
没索引的 WHERE 条件,InnoDB 可能锁整张表——这时“单行更新”只是幻觉。
应用层必须实现死锁重试,不能只靠数据库回滚
InnoDB 回滚死锁事务是正常机制,但应用不处理就等于流程中断。必须主动捕获 MySQL 错误码:
- MySQL 5.7+:错误码
1213(Deadlock found when trying to get lock) - MySQL 8.0+:也可能是
1205(Lock wait timeout exceeded在某些配置下) - 重试策略建议指数退避:首次延迟 10ms,第二次 30ms,第三次 100ms,最多 3 次
- 禁止在重试逻辑里嵌套外部调用(如 HTTP 请求、Redis 写入),否则事务持锁时间不可控,放大问题
真正难处理的不是死锁本身,而是那些看似“偶尔失败、重试又成功”的 case——它们暴露的是事务边界模糊、锁范围失控、或索引失效等深层设计问题。


















