死锁源于多事务加锁顺序不一致,解决方法是硬编码统一加锁顺序(如按表名字母序),拆分多表UPDATE为单表语句并显式加锁,跨库场景需应用层协调。

死锁不是随机发生的,是加锁顺序不一致导致的
多个事务同时更新多张表时,如果各自按不同顺序获取行锁(比如事务 A 先锁 users 再锁 orders,事务 B 反过来),就极易触发 MySQL 的死锁检测并回滚其中一个。这不是并发太高或索引没建好,纯粹是锁序混乱。
解决思路很直接:所有涉及多表更新的逻辑,必须约定同一套加锁顺序。不是“尽量统一”,而是代码里硬编码成固定顺序。
- 优先按表名字母序排序(如
accounts→orders→users),简单、无歧义、易审查 - 避免用业务逻辑决定顺序(例如“先更新主表再更新子表”),因为主/子关系在不同场景下可能反转
- 如果使用 ORM,注意它生成的 SQL 顺序是否可控;Django 的
select_for_update()、SQLAlchemy 的with_for_update()都依赖你显式控制查询顺序
UPDATE 多表时,MySQL 实际加锁顺序由执行计划决定
很多人以为写 UPDATE t1, t2 SET ... WHERE t1.id = t2.t1_id 就等于“先锁 t1 再锁 t2”,但 MySQL 会根据统计信息重排驱动表,实际加锁顺序可能相反。这会让“表面统一”的顺序失效。
真正可控的方式,是拆成单表语句,并显式按约定顺序执行:
START TRANSACTION; UPDATE users SET status = 'paid' WHERE id = 123 LOCK IN SHARE MODE; UPDATE orders SET paid_at = NOW() WHERE user_id = 123 LOCK IN SHARE MODE; COMMIT;
-
LOCK IN SHARE MODE或FOR UPDATE必须出现在每个UPDATE语句末尾,不能只靠 WHERE 条件隐式加锁 - 不要依赖
SELECT ... FOR UPDATE后再UPDATE—— 中间可能被其他事务修改,还得重新校验 - 如果必须用多表 UPDATE,用
EXPLAIN FORMAT=tree确认驱动表,并在注释里写明“此语句依赖 t1 为驱动表”,方便后续维护者识别风险
分布式场景下,单库加锁顺序无法覆盖全局
微服务拆分后,users 和 orders 可能不在同一个数据库,甚至不同实例。这时靠 SQL 加锁顺序完全失效——你锁不住另一个库的行。
此时必须退到应用层协调:
- 引入幂等 key + 状态机(例如先插入
payment_attempts(id='pay_abc', status='pending'),再异步驱动各库更新) - 用分布式锁(如 Redis 的
SET key value NX PX 10000)包裹整个跨库操作,锁 key 按固定顺序拼接:"lock:users:123:orders:456" - 禁止在事务内调用下游 HTTP 接口——网络延迟会让锁持有时间不可控,大幅提升死锁概率
ORDER BY + LIMIT 不保证加锁范围,别拿来“假装排序”
有人试图用 SELECT * FROM orders WHERE user_id = 123 ORDER BY id LIMIT 1 FOR UPDATE 来“确保只锁一行”,但若 id 上无索引,MySQL 可能锁住整个索引区间;即使有索引,ORDER BY 也不影响加锁顺序规则本身。
关键点在于:锁顺序 ≠ 查询顺序。锁的是满足 WHERE 的所有行,而顺序只是结果集排列方式。
- 检查
EXPLAIN输出里的key和rows,确认是否走了预期索引 - 多条件查询时,联合索引字段顺序必须匹配 WHERE 子句最左前缀,否则可能全表扫描加锁
- 用
SELECT ... FOR UPDATE时,如果 WHERE 条件不精确(如用LIKE '%abc'),MySQL 很可能升级为间隙锁或临键锁,扩大锁定范围
实际落地最难的不是写对 SQL,而是让所有服务、所有开发者、所有定时任务都遵守同一套锁序规则。一旦某处漏掉,死锁就会在流量高峰时精准复现——而且报错日志里只显示“Deadlock found when trying to get lock”,不会告诉你哪段代码坏了规矩。










