死锁由事务加锁顺序不一致引发循环等待,与并发量无关;即使单行UPDATE、两个事务也可能因前置锁操作或隐式锁导致A锁1等5、B锁5等1而死锁。

死锁不是并发高导致的,而是事务加锁顺序不一致触发的循环等待——哪怕只有两个事务、更新同一行,也会立刻报 Deadlock found when trying to get lock 或 Error 1205。
UPDATE 同一行为什么还会死锁
很多人误以为“只改 id=1 这一行”,就一定串行执行。但实际中,只要事务里混入其他锁操作(比如前置的 SELECT ... FOR UPDATE、JOIN 查询、触发器或隐式锁),就可能让两个事务以相反顺序获取锁:
- 事务 A:先锁
id = 1,再试图锁id = 5 - 事务 B:先锁
id = 5,再试图锁id = 1
结果是 A 持有 1 等 5,B 持有 5 等 1 → InnoDB / SQL Server 立即检测并回滚其中一个。注意:即使两条语句都是 UPDATE t SET x = ? WHERE id = 1,若它们嵌套在不同逻辑路径(如子查询、ORM 自动生成的 JOIN)中,锁获取时机仍可能错位。
ORDER BY 不等于加锁顺序确定
ORDER BY 只有在真正走索引排序时才影响加锁顺序;否则优化器全表扫描 + Using filesort,锁行顺序完全随机,根本无法防死锁。
- 错误写法:
UPDATE t SET status = 'done' WHERE type = 'job' ORDER BY id,但type字段没索引 →EXPLAIN显示type: ALL→ 锁所有匹配行,顺序不可控 - 正确验证方式:
EXPLAIN FORMAT=TRADITIONAL SELECT * FROM t WHERE type = 'job' ORDER BY id,必须看到key非空、Extra不含Using filesort - 复合索引要满足最左前缀:有
(type, id)才能生效;只有(id, type)则无效
用原子 UPDATE + WHERE 条件替代 SELECT FOR UPDATE
“先查再更”是死锁温床:查完到更新之间存在时间窗口,且 SELECT ... FOR UPDATE 在无记录时还会加间隙锁(Gap Lock),多个并发请求争抢同一间隙,极易死锁。
- 危险模式:
SELECT * FROM stock WHERE sku = 'ABC' FOR UPDATE→ 计算 →UPDATE stock SET qty = ? WHERE sku = 'ABC' - 推荐写法:
UPDATE stock SET qty = qty - 1 WHERE sku = 'ABC' AND qty >= 1,执行后检查影响行数(ROW_COUNT()/@@ROWCOUNT)为 0 即说明失败,无需锁、不依赖隔离级别 - 前提:确保
sku有唯一索引,否则WHERE sku = 'ABC'会升级为间隙锁 - 避免在
WHERE中用函数:WHERE UPPER(sku) = 'ABC'会让索引失效,退化为全表扫描加锁
多表更新必须统一访问顺序
交叉更新是高频死锁源:事务 A 先 UPDATE users 再 UPDATE orders,事务 B 反过来,就构成循环等待链。
- 约定所有业务代码按固定顺序访问表,例如“总是先 users,再 orders,再 logs”
- 批量更新多行时,显式加
ORDER BY pk并确保该字段有索引,让所有事务按相同物理顺序加锁 - 避免在事务内混非数据库操作(如 HTTP 调用、文件读写),防止锁长时间持有
- 长事务显著扩大锁窗口,应把事务控制在最小必要范围,尽快提交
真正难处理的不是“怎么加锁”,而是“怎么绕过锁”——比如用 INSERT ... ON DUPLICATE KEY UPDATE 替代条件判断,或用乐观锁(version 字段)把冲突转移到应用层重试。这些方案不依赖数据库锁机制,也就不受加锁顺序干扰。

















