死锁本质是事务以不同顺序加锁同一组资源导致的循环等待,预防核心是统一访问顺序;update/delete必须走索引且where条件符合最左前缀原则;批量操作须按主键升序处理;事务粒度宜小,避免外部调用阻塞锁。

死锁不是配置问题,是访问顺序冲突
MySQL 死锁无法通过调大 innodb_lock_wait_timeout 或关掉自动提交来“避免”,它本质是多个事务以不同顺序加锁同一组资源(比如两行记录、两个索引项)导致的循环等待。预防的核心是让所有事务按**相同顺序**访问资源。
UPDATE/DELETE 必须走索引,且顺序要可控
无索引的 UPDATE 或 DELETE 会触发全表扫描+行锁升级,锁住大量无关记录,极大增加死锁概率。更危险的是:优化器可能因统计信息过期或参数变化,某次走索引、下次走全表,导致访问顺序不一致。
- 确保 WHERE 条件中所有字段都落在同一个复合索引的最左前缀上(例如索引
(user_id, status, created_at),WHERE 中用user_id = ? AND status = ?是安全的;只用status = ?就可能失效) - 用
EXPLAIN检查每条 DML 的执行计划,确认key列非 NULL,rows值稳定且合理 - 避免在事务中先
SELECT ... FOR UPDATE再UPDATE,除非你能保证 SELECT 返回的行序与后续 UPDATE 的 WHERE 顺序严格一致(通常很难)
批量操作必须按主键升序处理
当业务需要更新一批记录(如订单状态批量变更),若直接用 IN (100, 5, 88),InnoDB 内部可能按任意顺序加锁——而另一个事务恰好用 IN (5, 88, 100),就构成死锁条件。
- 应用层先查出 ID 列表,显式排序:
ORDER BY id ASC - 拆成小批量(如每次 100 行),用
WHERE id IN (?,?,?)+ORDER BY id(虽然 IN 本身不保序,但配合 ORDER BY 和唯一主键,InnoDB 会按主键物理顺序加锁) - 极端场景下,改用逐条
UPDATE ... WHERE id = ?并按 ID 升序执行,牺牲吞吐换确定性
事务粒度越小越好,别在事务里调外部服务
一个持有行锁的事务如果卡在 HTTP 调用、文件读写或 sleep 上,等于把锁占着不动,其他事务只能干等——这不是死锁,但会显著放大死锁发生率(因为等待窗口变长,更多事务挤进同一资源竞争)。
- 把日志记录、消息发送、缓存更新等非数据库操作移到事务 提交之后
- 避免在事务中做任何不可控耗时操作;如果必须查外部 API,先查好数据再进事务
- 用
SELECT ... FOR UPDATE NOWAIT主动失败,比无限等待更利于快速重试(需捕获Lock wait timeout exceeded错误)
真正难防的死锁往往藏在看似无关的二级索引更新里——比如你只更新 status 字段,但该字段上有索引,InnoDB 就要同时维护聚簇索引和二级索引的锁。这时候光看主键顺序不够,得结合实际执行计划和索引结构一起看。










