行级锁死锁是怎么发生的?
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或慢日志,找到它背后真正拖慢事务的元凶。
