死锁主因是事务交叉更新表顺序不一致,而非并发高;须统一多表操作顺序、避免长事务与范围更新、慎用on duplicate key update,并通过show engine innodb status定位具体冲突。

死锁不是并发太高,是事务交叉更新不同表的顺序不一致
MySQL 死锁绝大多数时候和 QPS 无关,而是两个事务以相反顺序修改同一组表。比如事务 A 先 UPDATE users 再 UPDATE orders,事务 B 却先 UPDATE orders 再 UPDATE users——只要它们恰好在临界点卡住,InnoDB 就会选一个回滚,报出 Deadlock found when trying to get lock。
这种场景在微服务里特别常见:用户服务和订单服务各自按自己逻辑写 SQL,没人协调访问顺序。
- 用
SHOW ENGINE INNODB STATUS查看最近死锁,重点看*** (1)和*** (2)两段里的WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S),能直接看出哪两张表、哪几行被交叉持有 - 业务代码里所有涉及多表更新的事务,必须约定全局统一的表访问顺序(例如:总是
users → orders → order_items) - 避免在事务里调用外部 HTTP 接口或执行耗时计算,否则会拉长锁持有时间,放大交叉概率
唯一索引冲突 + INSERT … ON DUPLICATE KEY UPDATE 容易触发隐式锁升级
这个组合看似安全,实则是死锁高发区。当两个事务同时对同一个 UNIQUE 字段做 INSERT ... ON DUPLICATE KEY UPDATE,InnoDB 会在唯一索引上加 GAP 锁+插入意向锁,再试图升级为记录锁——如果双方都卡在 GAP 锁阶段,又互相等待对方释放,就死锁了。
- 检查表结构:确认冲突字段是否真需要
UNIQUE约束;如果不是强业务要求,改用应用层幂等(如先SELECT再INSERT)更可控 - 如果必须用
ON DUPLICATE KEY UPDATE,确保 WHERE 条件走的是聚簇索引(主键),而不是二级唯一索引——后者更容易产生不可预测的 GAP 锁范围 - 线上已出现该死锁时,临时降级方案是加
SELECT ... FOR UPDATE显式锁住主键行,把冲突前置到读阶段,但要注意锁粒度和性能影响
长事务 + 范围条件 UPDATE 是死锁温床
UPDATE ... WHERE created_at > '2024-01-01' 这类语句,在未命中索引或索引区分度低时,可能锁住成百上千行,甚至整个索引区间。另一个事务若恰好更新其中某几行,就极易陷入死锁。
- 用
EXPLAIN确认该 UPDATE 是否走了预期索引;没走的话,优先优化索引,而不是加FOR UPDATE或拆事务 - 避免在事务中执行范围大、耗时长的 UPDATE;可拆成小批量(如每次 100 行),用
WHERE id BETWEEN ? AND ?配合主键定位,减少单次锁持有量 - 监控
innodb_trx.trx_state = 'LOCK WAIT'和trx_started时间,及时发现运行超 5 秒的锁等待事务
READ COMMITTED 隔离级别不能避免死锁,但能减少锁数量
有人以为换成 READ COMMITTED 就能绕过死锁,其实不然——死锁只和加锁顺序与资源竞争有关,和是否生成 MVCC 版本无关。但它确实能禁用 GAP 锁(除外键和唯一索引冲突场景),从而缩小锁覆盖范围。
- 确认当前隔离级别:
SELECT @@transaction_isolation;生产环境建议统一设为READ COMMITTED,除非业务强依赖REPEATABLE READ的一致性快照语义 - 即使用了
READ COMMITTED,UPDATE仍会对匹配行加记录锁,多表交叉更新照样死锁 - 不要为了“防死锁”而盲目调低隔离级别;真正有效的是控制事务长度、固化访问顺序、消除非必要范围锁
死锁本身不可怕,可怕的是把它当成随机事件忽略。一旦出现,必须定位到具体哪两条 SQL、哪两张表、哪个索引字段在打架——日志里的 WAITING FOR THIS LOCK TO BE GRANTED 行,比任何监控指标都真实。











