根本原因是多表更新顺序不一致导致InnoDB加锁成环;必须按表名字母序或业务主次统一加锁顺序,禁用条件分支动态拼表序,且所有访问须走唯一索引或主键。

UPDATE关联表时为什么容易死锁
根本原因不是SQL写得“复杂”,而是多表更新顺序不一致。比如事务A先UPDATE order再UPDATE user,事务B反过来,InnoDB加锁时就形成环路——谁也不放锁,MySQL只能选一个回滚(错误码 1213)。这种死锁占比超四成,和索引是否到位、隔离级别高低关系反而较小。
关联UPDATE必须按固定顺序加锁
所有涉及多表的UPDATE或SELECT FOR UPDATE,必须约定且强制执行统一顺序。这不是建议,是规范底线:
- 按表名**字母序**:如
order_item→order→user(不能颠倒) - 按业务主次:如核心订单流必须是
order→order_item→payment,下游表不能提前锁 - 动态拼接SQL时禁用条件分支控制表顺序,例如不能写
if (type == 'refund') { update user; update order; } else { update order; update user; } - 用
EXPLAIN FORMAT=TRADITIONAL确认每张表的访问都走了**唯一索引或主键**,否则SELECT FOR UPDATE可能升级为间隙锁甚至全表扫描锁
事务里别混进非DB操作
HTTP调用、Redis读写、日志落盘、sleep(),这些都会拉长锁持有时间。一个本该50ms完成的UPDATE JOIN,如果裹着300ms的第三方API响应,死锁概率指数级上升:
- 事务只保留「查→算→写」三步:用
SELECT FOR UPDATE读数据 → 本地判断逻辑 → 单条或多条UPDATE写回 - 外部依赖全部移出事务块,哪怕要重试也应在事务外做
- 批量更新别用循环
UPDATE,改用INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO合并为单语句 -
innodb_lock_wait_timeout不要调小(如设成2秒),它掩盖问题;线上保持默认50秒,靠监控捕获Lock wait timeout exceeded告警定位热点行
READ COMMITTED能减锁但要防丢失更新
把隔离级别从默认REPEATABLE READ降到READ COMMITTED,能去掉间隙锁,大幅缩小锁范围。但它不解决“先读后写”导致的覆盖问题:
- 若业务允许丢失更新(如统计类计数),可直接切隔离级别
- 若不能丢,必须加校验:在
UPDATE的WHERE子句中带上原始值或version字段,例如UPDATE order SET status = 'paid', version = version + 1 WHERE id = 123 AND version = 5 - 避免
UPDATE ... JOIN中对非驱动表使用非唯一条件,否则可能锁住大量无关行
SELECT FOR UPDATE——它锁的不是一行,是一整个索引区间。


















