深蓝黑背景品牌封面:左侧大标题「行锁、间隙锁、死锁」,副标题「InnoDB 锁机制与排查一次讲透」;右侧原创装饰——一条索引记录横条上排布五枚记录块,其中两枚之间画一枚括号形锁标记住缝隙,另有一枚挂锁图标扣在中间记录上,寓意锁不只锁行、也锁行与行之间的间隙;左上标注「知识专栏 · 数据库锁」,右上标注「Day 09 · 10-17」 图 1:本文封面,锁不只锁行,也锁行与行之间的缝隙。

凌晨两点,告警群里跳出一条「数据库死锁,事务已回滚」。白天刚在面试里背完「乐观锁、悲观锁、读写锁」的人,这时多半会愣住:策略谱系背得再熟,对着一条死锁日志还是不知道先看哪。这篇是锁面试题的延伸篇——锁策略的谱系与选型之前已经讲过,这次钻进 InnoDB 引擎内部,把行锁、间隙锁、Next-Key Lock 各锁什么讲清楚,再学会读死锁日志。

核心结论一句话

InnoDB 的行级锁锁的是索引记录,不是物理行。默认的可重复读隔离级别下,搜索和索引扫描实际用的是 Next-Key Lock——记录加它前面的缝隙一起锁,这才是防止幻读的机关。而死锁不是 bug,是数据库的自我保护:检测到等待环就牺牲一个事务回滚,工程师要做的是读懂日志、降低环出现的频率,而不是幻想消灭它。

三种行级锁,锁的范围各不同。

假设索引上有五条记录 10、20、30、40、50。记录锁(Record Lock)只扣住单条记录,比如用唯一索引等值锁定 30,别的事务不能改也不能删这一行。间隙锁(Gap Lock)锁的是两条记录之间的开区间,比如罩住 20 与 30 之间的缝隙,不碰记录本身,唯一职责是挡住别的事务往缝隙里插入新值。Next-Key Lock 是前两种的组合:左开右闭的区间,记录加缝隙一起锁。可重复读级别下,范围查询扫描到的每一段都是这个单位,所以它是 InnoDB 行锁的默认形态。

为什么要锁缝隙?为了幻读。

可重复读承诺同一个事务里两次相同的范围查询结果一致。只锁已存在的记录挡不住「插进来一条新的」:第一次查 20 到 30 之间是空的,别的事务插入了 25,第二次查就多出一行,这就是幻读。Next-Key Lock 把缝隙也锁上,插入 25 的事务会被挡住,幻读就此防住。代价同样在这里:范围锁让并发插入互相等待,热点区间写冲突变多。有两个边界事实值得记住:用唯一索引等值命中已存在的行时,Next-Key Lock 会退化为纯记录锁,不锁缝隙;把隔离级别降到读已提交,缝隙锁基本退场(只在外键检查与重复键检查时保留),很多高并发业务正是用这个降级换并发度。

结构加环图:上部是一条索引记录轴,五个记录点 10、20、30、40、50 依次排开,用三种括号演示锁范围——记录锁只扣住单条记录 30;间隙锁罩住 20 与 30 之间的开区间,不碰记录本身;Next-Key Lock 罩住 20 到 30 的左开右闭区间,记录加缝隙一起锁,是默认单位。下部左侧演示幻读对策——范围查询时 Next-Key Lock 挡住别的事务往缝隙里插入新记录。下部右侧是死锁环——事务 A 持记录 10 的锁等记录 30,事务 B 持记录 30 的锁等记录 10,两个箭头首尾相接成环,数据库检测到环就牺牲其中一个回滚。底部提示:读死锁日志先找「各持什么、各等什么」,环一画出来原因就清楚了 图 2:三种锁的锁定范围与死锁环的画法。

死锁日志怎么读:各持什么,各等什么。

SHOW ENGINE INNODB STATUS 的输出里有一节 LATEST DETECTED DEADLOCK,记录最近一次死锁:涉及哪些事务、各自正在执行什么语句、各持有什么锁、各在等什么锁、最后谁被回滚。读法是先画环:事务 A 持记录 10 的锁、等记录 30;事务 B 持记录 30、等记录 10——两个箭头首尾相接,环就成立了。再看被牺牲的是谁:数据库倾向回滚代价小的事务。日志里的锁模式字段直接对应三种锁:locks rec but not gap 是纯记录锁,只写 lock_mode X 的往往带着缝隙,是 Next-Key Lock。把这两个字段认熟,日志就从天书变成了结构化的案情记录。

治理死锁:不是消灭,是降频。

偶发死锁被自动回滚不必紧张,频发才要治理,三板斧按顺序用。第一,统一加锁顺序:所有事务按固定顺序访问多张表、多行记录,环从根上凑不成。第二,缩短事务:把慢查询、远程调用挪出事务边界,锁持有时间越短,撞车概率越低。第三,审视索引:没走索引的 UPDATE 会锁住扫描到的每一条记录,锁范围从行膨胀成区间甚至更大,这是许多「莫名其妙的死锁」的真凶,补上合适的索引往往比改代码见效快。

三个常见误区。

误区一:「行锁就是锁一行。」它锁的是索引记录,默认还带着前面的缝隙,实际范围比直觉大。误区二:「两个事务的间隙锁会互斥。」间隙锁是纯抑制性的,多个事务可以在同一条缝隙上同时持有间隙锁,它只负责挡插入,彼此不冲突。误区三:「死锁要清零。」零死锁往往意味着把并发降到接近没有,合理目标是死锁率可控、被回滚的事务有自动重试兜底。

给你的行动建议。

找一个测试库,开两个会话按相反顺序更新同两行数据,亲手制造一次死锁,然后把日志里的环画出来、把被牺牲的事务找出来。练过这一次,下次告警再响,你就是群里最镇定的那个。

你在线上遇到过死锁吗?最后是靠统一加锁顺序、缩短事务还是补索引解决的?欢迎分享你的排查过程。