深入剖析MySQL锁机制:从全局锁到行锁,一文读懂

发布时间:2026/7/2 1:35:53
深入剖析MySQL锁机制:从全局锁到行锁,一文读懂 从一道面试题说起面试官问“MySQL 的可重复读隔离级别是怎么解决幻读的”很多人脱口而出“用间隙锁。”面试官追问“那间隙锁是怎么工作的什么情况下会产生间隙锁能给我举个例子吗”很多人开始支支吾吾。这篇文章的目标就是让你彻底搞懂这几个问题。我们不堆概念直接上案例。一、锁的底层逻辑为什么行锁实际上是索引锁先记住一个核心结论InnoDB 的锁永远是加在索引上的而不是加在数据行上的。这是理解一切行锁规则的前提。InnoDB 的表是索引组织表——数据本身就是按主键索引组织的一棵 B 树聚簇索引的叶子节点存储完整的行数据。当你执行SELECT * FROM user WHERE id 1 FOR UPDATE时InnoDB 实际上是在主键索引的 B 树上找到id1这个索引项然后在这个索引项上加锁。这个结论为什么重要因为如果查询条件走了索引InnoDB 就能精确定位到要锁的索引项实现行锁。如果查询条件没走索引InnoDB 不知道要锁哪些行只能扫描全表把扫描到的每一个索引记录都加锁——行锁退化为表锁。所以索引设计直接决定了锁的粒度。这不是一句空话。二、用案例吃透三种行锁行锁不是只有一种。InnoDB 的行锁分成三类记录锁Record Lock、间隙锁Gap Lock和临键锁Next-Key Lock。我们用一张表来理解这三者的区别。假设有一张user表id是主键已有的数据是id135101. 记录锁锁住某一行执行SELECT * FROM user WHERE id 3 FOR UPDATE。InnoDB 在主键索引上找到id3这条记录只锁这一条。其他事务可以插入id2、id4也可以更新id5唯独不能动id3。这就是记录锁最单纯的行锁。2. 间隙锁锁住“空隙”执行SELECT * FROM user WHERE id BETWEEN 4 AND 6 FOR UPDATE。注意id4和id6在表中都不存在。那 InnoDB 锁什么它锁的是索引记录之间的间隙——也就是(3, 5)和(5, 10)这两个区间。在这个范围内其他事务不能插入新数据。所以如果你在另一个事务里执行INSERT INTO user VALUES (4)或INSERT INTO user VALUES (6)都会被阻塞。这就是间隙锁——锁住的是“空隙”不是记录本身。3. 临键锁记录锁 间隙锁在 RR 隔离级别下InnoDB默认使用的就是临键锁。临键锁 记录锁 它前面的间隙锁。它的锁定范围是左开右闭区间——锁住前面的间隙也锁住当前记录。比如执行SELECT * FROM user WHERE id 5 FOR UPDATE在 RR 级别下锁定id5这条记录本身记录锁同时锁定(3, 5]这个区间间隙锁这样一来其他事务既不能修改id5也不能在3和5之间插入新数据。临键锁的核心目的就是防止幻读。三、一个让你彻底理解“幻读 vs 间隙锁”的案例幻读是什么一个事务在两次查询之间看到了其他事务插入的新数据。RR 级别下普通的SELECT走的是 MVCC多版本并发控制看到的是快照不会有幻读。但当前读SELECT ... FOR UPDATE、UPDATE、DELETE不一样——它读到的是最新数据。来看一个经典场景。场景user表有id为 1、3、5、10 四条记录。事务 ABEGIN; SELECT * FROM user WHERE id 2 FOR UPDATE; -- 返回 id3,5,10事务 B在事务 A 提交之前INSERT INTO user VALUES (4);如果没有任何锁保护事务 B 的插入会成功。然后事务 A 再次执行SELECT * FROM user WHERE id 2 FOR UPDATE会看到 id4——幻读发生了。间隙锁如何阻止在 RR 级别下事务 A 的SELECT ... WHERE id 2 FOR UPDATE会加什么锁它会扫描从id2之后的所有索引记录每扫到一个就加临键锁扫描到id3锁定(2, 3]扫描到id5锁定(3, 5]扫描到id10锁定(5, 10]扫描到最后锁定(10, ∞)这些间隙全被锁住了。事务 B 想插入id4而4落在(3, 5]这个区间里——这个区间被事务 A 的临键锁锁住了。插入被阻塞。幻读被消灭了。代价是什么锁的范围变大了。如果你的表里有 100 万条数据一个范围查询可能锁住几十万个间隙并发性能急剧下降。四、隔离级别决定了锁的行为我把四个隔离级别下的锁行为总结成一张表隔离级别间隙锁锁释放时机解决的问题主要风险读未提交RU无语句结束无脏读、幻读读已提交RC无语句结束脏读幻读可重复读RR有事务结束脏读、不可重复读、幻读锁冲突多、死锁风险高串行化有事务结束全部并发极差关键结论RC级别没有间隙锁锁的范围更小并发性能更高。这也是很多互联网公司的首选。RR级别有间隙锁能彻底解决幻读但代价是更多的锁冲突和死锁风险。如果你的业务能接受 RC 级别比如大部分互联网业务降级到 RC 可以大幅减少间隙锁导致的死锁和锁等待。五、死锁当两个事务互相等对方死锁的本质是循环等待。最经典的死锁模式是ABBA事务 AUPDATE user SET namea WHERE id1; -- 锁住 id1 UPDATE user SET nameb WHERE id2; -- 等待 id2事务 BUPDATE user SET namec WHERE id2; -- 锁住 id2 UPDATE user SET named WHERE id1; -- 等待 id1A 等 B 释放 id2B 等 A 释放 id1——死锁。InnoDB 能自动检测死锁并回滚持有最少排他锁的事务。但频繁死锁依然会影响业务。如何预防统一加锁顺序多个事务访问多张表时约定相同的访问顺序。尽早加锁在事务一开始就用SELECT ... FOR UPDATE把需要的锁都拿到。缩小事务范围事务越长持有锁的时间越长死锁概率越高。确保查询走索引否则行锁变表锁死锁概率指数上升。六、写在最后回顾一下核心脉络行锁加在索引上不是加在数据行上——索引决定锁的粒度。记录锁锁行间隙锁锁空隙临键锁两者都锁——RR 级别默认用临键锁。间隙锁是RR级别解决幻读的关键武器也是死锁的头号元凶。隔离级别决定锁的行为——RC 无间隙锁并发更高RR 有间隙锁一致性更强。理解这些之后再回头看那个面试题——“可重复读怎么解决幻读”你应该能说出RR 级别下当前读操作会加临键锁记录锁间隙锁锁住查询范围内所有索引记录及其间的间隙阻止其他事务在这些间隙中插入数据从而防止幻读。而不是简单地回答“用间隙锁”。锁的本质是用并发性能换数据一致性。理解了这个权衡你就能在业务场景中做出更明智的选择。希望这篇文章能让你对 MySQL 的锁有一个质的理解。如果觉得有收获欢迎点赞、收藏、转发。