行级锁死锁是因事务间循环等待索引行锁或间隙锁导致,InnoDB自动检测并回滚代价最小事务;根本原因包括未走索引致表级锁、间隙锁/Next-Key Lock在范围查询中扩大锁定范围,预防需确保索引访问、统一更新顺序、缩小事务粒度、合理降级隔离级别。

行级锁死锁是怎么发生的?
MySQL 的行级锁死锁,本质是两个或多个事务互相持有对方需要的锁、又在等对方释放,形成循环等待。InnoDB 会主动检测并回滚代价最小的那个事务,报错 Deadlock found when trying to get lock —— 这不是异常崩溃,而是引擎的正常干预机制。
关键点在于:行级锁 ≠ 安全锁。它只在**走索引**时生效;一旦查询没命中索引,InnoDB 会退化为锁整张表(或大量无关行),大幅增加冲突概率。
- 事务 A 执行
UPDATE users SET status=1 WHERE id=100(id是主键)→ 锁住第 100 行 - 事务 B 同时执行
UPDATE users SET status=2 WHERE name='alice'(name无索引)→ InnoDB 扫全表,锁住所有行(包括第 100 行) - 此时 A 等 B 释放第 100 行,B 等 A 提交释放全表锁 → 死锁触发
为什么加了索引还死锁?间隙锁(Gap Lock)和 Next-Key Lock 是隐形推手
在默认隔离级别 REPEATABLE READ 下,InnoDB 不仅锁匹配的行,还会锁住「索引间隙」——防止幻读。这意味着即使你查的是唯一值,也可能锁住前后一段范围。
例如表 t 有索引 idx_age,当前数据中 age 值为 20、25、30:
UPDATE t SET name='x' WHERE age BETWEEN 22 AND 28;
这条语句会锁住 age 在 (20,25) 和 (25,30) 两个间隙,以及 25 这一行(Next-Key Lock)。如果另一个事务正尝试插入 age=23 或更新 age=25,就可能卡住甚至死锁。
- 联合索引下更危险:
WHERE a=1(只用左前缀)仍会加 Gap Lock,哪怕a是唯一字段 -
SELECT ... FOR UPDATE或UPDATE在范围条件、LIKE 'abc%'、BETWEEN场景下极易触发间隙锁竞争 - 唯一索引等值查询(如
WHERE id=123)通常只锁单行,不锁间隙 —— 这是少数“安全”场景
避免死锁的四条实操铁律
死锁无法 100% 消除,但可压缩到业务可接受水平。重点不在“检测”,而在“预防设计”。
-
所有写操作必须走索引:用
EXPLAIN验证type字段不是ALL或index;缺失索引的WHERE条件,宁可加索引,也不接受全表扫描 -
统一访问顺序:多个事务更新多行时,强制按主键/索引升序处理。例如批量扣库存,先
ORDER BY sku_id ASC再遍历更新,避免 A 更新 (100,200),B 更新 (200,100) - 事务粒度要小:把“查 → 改 → 发消息 → 记日志”拆成多个短事务;尤其避免在事务里调外部 HTTP 接口或 sleep
-
读写分离 + 隔离级别降级:非强一致性场景,将隔离级别设为
READ COMMITTED(关闭间隙锁),配合应用层做重试逻辑
排查死锁:别只看报错,要看 SHOW ENGINE INNODB STATUS
MySQL 每次死锁后,都会把最近一次死锁详情写入引擎状态。这不是日志文件,而是内存快照,需手动抓取:
SHOW ENGINE INNODB STATUS\G
重点关注 LATEST DETECTED DEADLOCK 区块,它会明确列出:
- 哪个事务持有哪些锁(
HELD LOCKS) - 哪个事务在等哪一行(
WAITING FOR THIS LOCK TO BE GRANTED) - 涉及的 SQL、表、索引名、事务 ID、线程 ID
注意:该命令输出只保留最后一次死锁,且不记录历史。生产环境建议搭配监控脚本定期采集,否则问题复现后就再也看不到上下文了。
最常被忽略的一点:死锁往往不是孤立事件,而是高并发下某个慢查询或缺失索引被反复触发的结果。盯着那条报错 SQL 改,不如顺着 SHOW PROFILE 或慢日志,找到它背后真正拖慢事务的元凶。










