死锁源于事务访问资源顺序不一致,而非锁使用错误;应确保所有事务按主键或唯一索引的确定性顺序(如order by id asc)访问行,并避免范围查询引发的隐式间隙锁。

死锁不是锁用错了,而是事务访问资源的顺序不一致
MySQL 中的死锁几乎都源于多个事务以不同顺序加锁访问同一组行。InnoDB 会自动检测并回滚其中一个事务(报错 Deadlock found when trying to get lock),但频繁死锁说明业务逻辑或 SQL 组织存在隐患。
关键不是“怎么加锁”,而是“怎么让所有事务按相同顺序访问行”。比如两个事务都先更新 user_id=100 再更新 user_id=200,就不会死锁;但如果一个按 ID 升序、另一个按降序,就极易触发。
- 始终按主键或唯一索引的**确定性顺序**组织 DML:例如用
ORDER BY id ASC显式排序后再UPDATE或SELECT ... FOR UPDATE - 避免在应用层分批处理时「凭感觉」取数据:比如分页查出一批 ID 后再循环更新,若两次查询结果顺序不一致(如没加
ORDER BY),就可能打乱加锁顺序 - 批量操作尽量用单条语句完成,而不是拆成多条独立的
UPDATE:一条UPDATE ... WHERE id IN (1,5,3)的加锁顺序由 InnoDB 内部按主键排序决定;而三条单独的UPDATE则完全取决于执行顺序
SELECT ... FOR UPDATE 和 UPDATE 的加锁行为差异必须清楚
很多人以为 SELECT ... FOR UPDATE 和 UPDATE 加的是同一种锁,其实不然——前者是否加间隙锁(gap lock)、是否升级为临键锁(next-key lock),高度依赖 WHERE 条件是否命中索引、是否是唯一查找。
比如表 t 有主键 id,执行:
SELECT * FROM t WHERE id = 10 FOR UPDATE;
只锁住 id=10 这一行(记录锁);但执行:
SELECT * FROM t WHERE id > 5 AND id < 15 FOR UPDATE;
会锁住 (5,15) 区间(间隙锁 + 可能的临键锁),这时如果另一个事务想插入 id=12,就会被阻塞——而你可能根本没意识到这个隐含的范围锁。
- 尽量用等值条件 + 唯一索引做
SELECT ... FOR UPDATE,避免范围扫描 - 确认是否真的需要
SELECT ... FOR UPDATE:如果是为防并发修改,有时直接用带条件的UPDATE更安全(例如UPDATE t SET status='processing' WHERE id=10 AND status='ready'),失败即说明已被抢 - 开启
innodb_locks_unsafe_for_binlog=OFF(默认)时,普通UPDATE也会加间隙锁;但如果你用的是READ-COMMITTED隔离级别,间隙锁会被禁用——这点常被忽略
事务粒度越小越好,别把锁“捂”在手里太久
死锁概率随事务持有锁的时间呈非线性上升。哪怕加锁顺序完全一致,只要一个事务在 SELECT ... FOR UPDATE 后做了耗时操作(比如调外部 HTTP、写日志、复杂计算),另一个事务就可能在等待中被卷入死锁链。
- 把锁相关操作尽量靠近事务末尾:先做查询判断、本地计算,最后一步才
SELECT ... FOR UPDATE或UPDATE - 避免在事务中调用不可控的外部服务;如有必要,拆成「预占位 → 外部执行 → 确认提交」三阶段,用状态字段控制
- 监控
innodb_row_lock_time_avg和innodb_deadlocks状态变量,及时发现长事务或高频死锁点
用 SHOW ENGINE INNODB STATUS 定位真实死锁现场
报错信息里只有一句 Deadlock found when trying to get lock,真正有用的是 MySQL 自动记录的最近一次死锁详情,必须手动查:
SHOW ENGINE INNODB STATUS\G
输出中 LATEST DETECTED DEADLOCK 段落会明确列出:
- 涉及的两个事务各自的 SQL
- 各自已持有的锁(HELD LOCKS)
- 正在等待的锁(WAITING FOR THIS LOCK TO BE GRANTED)
- 甚至哪一行被哪个事务锁着、锁类型(record lock, gap before rec)
- 不要只看报错事务的 SQL,重点对比「对方事务持有什么锁」和「本事务在等什么锁」——这才是顺序冲突的证据
- 注意
Trx has been waiting 2 sec这类提示:说明等待已久,大概率是上游事务卡住了,不是锁本身设计问题 - 生产环境建议开启
innodb_print_all_deadlocks=ON,把每次死锁都记到 error log,便于事后归因
死锁不是靠“加锁技巧”消除的,而是靠对 InnoDB 行锁机制的理解 + 对业务访问模式的收敛。最容易被忽略的一点是:你以为自己只锁了一行,其实 InnoDB 因为索引结构或隔离级别,悄悄锁了一片区间——这往往才是死锁的起点。










