按主键顺序更新能减少死锁,因innodb行锁基于聚簇索引物理顺序加锁,统一按id升序操作可确保所有事务加锁顺序一致,避免“a锁5等2、b锁2等5”的循环等待。

为什么按主键顺序更新能减少死锁
多个事务以不同顺序更新同一组行,是触发 ERROR 1213 (40001) 的最常见原因。比如事务 A 先 UPDATE users SET name='a' WHERE id=5,再 UPDATE users SET name='b' WHERE id=2;事务 B 反过来先改 id=2 再改 id=5 —— 两者就可能互持锁、互相等待。
InnoDB 行锁基于索引加锁,而主键索引(聚簇索引)的物理顺序天然有序。只要所有事务都按 ORDER BY id 排序后批量处理,就能保证加锁顺序一致,从源头切断循环等待链。
- 批量更新前,先
SELECT id FROM table WHERE ... ORDER BY id拿到有序 ID 列表 - 再用
UPDATE ... WHERE id IN (1,2,3)或逐条按序执行(避免 IN 列表过大导致锁升级) - 如果用
SELECT ... FOR UPDATE,必须带ORDER BY primary_key,否则结果集顺序不确定,两次执行锁的行顺序可能颠倒
缩短事务长度不是“快点提交”,而是砍掉非数据库操作
事务越长,锁持有时间越久,和其他事务撞上的概率指数级上升。但很多人误以为“把 SQL 写快点”就行——其实瓶颈常在事务里混入了不该有的东西。
- 禁止在事务内调用 HTTP API、发邮件、读写文件、做复杂计算
- 避免在事务中等待用户输入或外部响应(比如等 Redis 返回、等 MQ 确认)
- 把数据校验提前:比如余额检查,用普通
SELECT(一致性读)在事务外做;只在事务内做最终扣减 - 如果业务逻辑必须分步,考虑拆成多个短事务 + 幂等设计,而不是一个长事务包到底
哪些索引缺失会悄悄放大死锁风险
没有合适索引时,MySQL 可能退化为全表扫描,进而对大量无关行加间隙锁(gap lock),极大扩展锁范围。你只打算改 3 行,实际锁了 300 行,冲突面就宽了。
典型表现:WHERE 条件走不到索引,或者用了函数/隐式类型转换导致索引失效。
- 用
EXPLAIN检查所有写操作的执行计划,确认type是range、ref或更好,而非ALL - WHERE 中涉及的字段,尤其是高频更新的非主键条件(如
status = 'pending'),应建联合索引覆盖查询+排序 - 避免在索引字段上用
LIKE '%abc'、JSON_CONTAINS()等无法利用索引前缀的操作
重试本身会引发新死锁,除非你控制好节奏
应用层捕获 ERROR 1213 后重试,是必要手段,但裸重试(比如 while True: try: ... except: continue)反而会让问题更糟——多个客户端在毫秒级内同步重试,大概率再次卡在同一资源上。
- 必须加随机或指数退避:
time.sleep(0.01 * (2 ** retry_count) + random.uniform(0, 0.005)) - 重试上限设为 3 次,超过就抛出原始异常,避免掩盖设计缺陷
- 每次重试前必须新建事务(
BEGIN),不能复用已失败的连接上下文 - 如果事务里有外部副作用(如已调第三方接口),重试前需先幂等判断是否已成功,否则可能重复扣款
真正难的不是写重试代码,而是让每次重试都落在更安全的执行路径上——这要求 SQL 顺序稳定、索引可靠、事务边界干净。否则重试只是把死锁延迟几毫秒而已。











