死锁主因是加锁顺序不一致而非锁多;需统一按主键升序访问数据、慎用非唯一索引、缩小事务粒度、捕获重试死锁并加强线上监控。

死锁不是锁多了,而是加锁顺序不一致
MySQL 的行锁本身不会导致死锁,真正触发死锁的是多个事务以不同顺序访问同一组行。比如事务 A 先更新 id=1 再更新 id=5,事务 B 反过来先更新 id=5 再更新 id=1,InnoDB 检测到循环等待就会主动回滚其中一个事务,并报错:Deadlock found when trying to get lock。
避免的核心思路是:让所有事务按相同逻辑顺序访问数据。
- 按主键升序批量更新:对要操作的 ID 列表先
ORDER BY id排序,再逐条或批量处理 - 避免在应用层分批查再更新:比如先
SELECT ... FOR UPDATE查出 10 条,再循环更新——这容易因查询结果顺序与实际索引扫描顺序不一致埋下隐患 - 尽量用单条
UPDATE ... WHERE id IN (1,5,3)替代多条语句,InnoDB 内部会对IN列表按主键排序后加锁
唯一索引 vs 非唯一索引,加锁范围差别很大
同一个 WHERE 条件,在不同索引类型下,InnoDB 加的锁可能从“仅目标行”扩大到“间隙锁”甚至“临键锁”,直接影响并发行为和死锁概率。
例如执行 UPDATE users SET name='x' WHERE age=25:
- 如果
age是唯一索引:只锁匹配的行(record lock) - 如果
age是普通索引:会加临键锁(next-key lock),即锁住满足age=25的所有行 + 这些行之间的间隙,可能意外阻塞其他事务插入或更新 nearby 数据 - 如果
age没有索引:走全表扫描,每行都加 record lock,锁粒度最大,死锁风险显著上升
查死锁日志时注意看 SHOW ENGINE INNODB STATUS 输出里的 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: 和 *** (2) HOLDS THE LOCK(S):,能清楚看到各自持有哪些锁、等待哪把锁。
事务粒度太大,是死锁温床
一个事务里混着 SELECT、INSERT、UPDATE、外部 API 调用、复杂计算,持续时间越长,持有锁的时间就越长,和其他事务冲突的概率就越高。
- 把非数据库操作(如发邮件、调第三方服务)移出事务块,改用消息队列或异步任务
- 避免在事务中做耗时循环或大对象序列化/反序列化
- 读多写少场景下,考虑用
SELECT ... LOCK IN SHARE MODE替代FOR UPDATE,降低锁强度 - 确认是否真的需要可重复读(RR)隔离级别;部分业务用
READ COMMITTED能减少间隙锁,但要注意幻读是否可接受
监控和快速定位比预防更现实
完全杜绝死锁几乎不可能,尤其在高并发、多服务协作的系统里。比起花大量精力设计“绝对安全”的 SQL,不如建立快速响应机制:
- 应用层捕获
Deadlock found when trying to get lock错误,自动重试(带退避,比如 10ms、50ms、200ms) - 定期跑
SHOW ENGINE INNODB STATUS并提取最近死锁信息,用脚本聚合分析高频冲突的表和条件 - 在慢日志或性能 schema 中开启
innodb_print_all_deadlocks = ON,确保每次死锁都落盘,不依赖手动触发SHOW
最常被忽略的一点:开发环境很少压测复合事务路径,而线上真实请求链路往往跨多个微服务、多次 DB 调用,锁的累积效应只有在流量高峰才暴露。别只盯着单条 SQL 是否加锁合理。










