可重复读能不能防幻读?MVCC 和 Next-Key Lock 到底谁在起作用

这道题我见过太多人翻车

"MySQL 可重复读隔离级别下,会出现幻读吗?"

这题几乎是后端面试的必考项。大部分人张口就是"不会,因为 MVCC"。面试官追一句"那 MVCC 具体怎么防的",很多人就开始卡壳,或者干脆把 MVCC 和加锁混成一锅粥。

我自己第一次被问到这题是校招面某家电商公司,当时答得就是"不会,MVCC",然后被追问"那如果我用 SELECT ... FOR UPDATE 呢",直接懵了。后来花了挺长时间才把这条链路理清楚:MVCC 和加锁管的其实是两码事,一个管快照读,一个管当前读,答案不是简单的"会"或"不会",得分情况说。这篇把这条链路完整过一遍。

先分清两种读

InnoDB 里读分两种:

快照读 :普通的 SELECT,不加任何锁,读的是某个时间点的历史版本。

当前读SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE,以及 UPDATEDELETEINSERT 这些写操作,读的是最新版本,并且会加锁。

这两种读防幻读的机制完全不是一回事------前者靠 MVCC,后者靠 Next-Key Lock。混着讲,是大部分人卡壳的根源。

快照读怎么防幻读:MVCC

可重复读级别下,一个事务开启后第一次执行快照读时,会生成一个 ReadView,记录当前系统里"活跃"的事务 ID 列表。之后这个事务里所有的快照读,都复用同一个 ReadView------这就是"可重复读"名字的由来。

每一行数据在 InnoDB 里都有隐藏的 trx_id(最后修改它的事务 ID),还有一条通过 undo log 串起来的版本链,可以往回倒推出这行数据历史上每个版本长什么样。

判断某个版本对当前事务是否可见,大致是拿这行的 trx_id 去跟 ReadView 里记的那几个边界值和活跃事务列表比对:

  • 如果这个版本是当前事务自己改的,可见。
  • 如果 trx_id 小于 ReadView 里最小的活跃事务 ID,说明这个版本在生成 ReadView 之前就已经提交了,可见。
  • 如果 trx_id 在活跃事务列表里,说明改这行的事务在生成 ReadView 时还没提交,不可见,顺着 undo log 版本链往前找上一个版本继续判断。
  • 如果 trx_id 大于等于 ReadView 生成时的下一个事务 ID,说明这是"未来"才发生的修改,不可见。

因为 ReadView 全程复用,别的事务后来插入的新行,其 trx_id 必然比这个事务能看见的范围要新,所以在这个事务眼里,这行数据从来没出现过。同一个事务里查一百次,结果都一样------快照读层面,幻读被 MVCC 挡住了。

这也是为什么面试官问"可重复读怎么防幻读",标准答案第一步永远是先讲 MVCC。但只讲到这一步是不完整的,漏掉了当前读那部分。

当前读为什么还会有问题

MVCC 只对快照读生效。一旦事务里用了 FOR UPDATE、或者执行了 UPDATE/DELETE,读的就是最新数据,MVCC 直接失效。

这时候如果没有额外的锁机制,经典的幻读场景就会发生:

事务 A 执行 SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE,拿到 id=10 和 id=20 两行。事务 B 这时候插入一条 id=15 的记录并提交。事务 A 如果紧接着再查一次同样的条件,会发现多出来一行------这就是当前读语义下的幻读。

InnoDB 用 Next-Key Lock 堵住这个口子。

Next-Key Lock:记录锁 + 间隙锁

Next-Key Lock 不是单一的锁,是"记录锁(Record Lock)"加"间隙锁(Gap Lock)"的组合。记录锁锁住已存在的行本身,间隙锁锁住两条记录之间那段"空隙"。注意,间隙锁锁的是一个区间而不是某一行,所以别的事务在这个区间里插入新记录,会直接被挡住。

还是上面那个例子:表里已有 id=5、10、20、25 四条记录。事务 A 执行 WHERE id BETWEEN 10 AND 20 FOR UPDATE,InnoDB 会把 (10, 20] 这个区间连同两端记录一起锁住,也就是间隙 (5,10] 之后到记录 20 为止的这一整段。这时候事务 B 想插入 id=15,会命中这段间隙锁,直接被阻塞,一直等到事务 A 提交或回滚。

再往下追一句会更有意思:如果查询条件是唯一索引上的等值查询(比如 WHERE id=10 FOR UPDATE),命中了一条已存在的记录,Next-Key Lock 会退化成单纯的记录锁,不会锁间隙------因为唯一索引保证了这个值不可能再插入第二条,锁间隙没有意义。这个退化条件是面试官很爱拿来追问的细节。

另外插入操作本身还有个"插入意向锁(Insert Intention Lock)",本质是一种特殊的间隙锁,专门用来降低并发插入时的锁冲突------只要两个插入操作实际插入的位置不冲突,它们对同一个间隙的插入意向锁互相不阻塞。这个知道就行,面试很少往这么细问。

完整对答一遍

把上面这套逻辑串起来,面试时大概可以这样答。

先说结论:要分快照读和当前读两种情况。

快照读层面,可重复读级别下同一个事务全程复用同一个 ReadView,基于 trx_id 和 undo log 版本链做可见性判断,别的事务新插入的行对当前事务永远不可见,所以快照读不会有幻读。

当前读层面,MVCC 不生效,InnoDB 靠 Next-Key Lock(记录锁加间隙锁)把查询涉及的区间锁住,阻止其他事务在这个区间插入新记录,所以当前读也不会有幻读。但这依赖 InnoDB 的具体实现,SQL 标准定义的可重复读级别本身并不保证防幻读,MySQL 是在标准之上做了加强。

追问环节常见的坑:唯一索引等值查询命中已有记录时锁会退化成记录锁;READ COMMITTED 级别下没有间隙锁,所以那个级别下当前读也会有幻读。这两点答上,基本能把这题聊透了。

顺手说一句

这类连环追问的题,光背答案没用,得真正把链路理顺才经得起连续问下去。我自己后来准备面试的时候,除了啃官方文档,也会用面灵这类工具做几轮模拟追问练习,专门打磨哪个环节容易被问倒------工具是辅助,但这道题本身该弄懂的原理一点都不能少。

如果这道题你之前也是含糊带过,建议自己动手开两个事务窗口,用 EXPLAIN 实测一遍 Next-Key Lock 的加锁范围,比看多少篇文章都管用。

相关推荐
weedsfly1 小时前
Cookie 安全三属性:HttpOnly、Secure、SameSite 分别防什么?
前端·javascript·面试
AOwhisky2 小时前
Kubernetes(K8s)学习笔记(第十四期):集群存储与有状态应用(下篇):StatefulSet 有状态应用管理
redis·笔记·mysql·云原生·kubernetes·云计算·k8s
多年小白2 小时前
第八篇 模拟面试套卷
人工智能·ai·面试·职场和发展
zzz_23682 小时前
【Java实习面试算法冲刺】哈希!
java·算法·面试
EntyIU2 小时前
CentOS-高可用部署手册-MySQL双主RedisNginx
linux·mysql·centos
承渊政道2 小时前
【MySQL数据库学习】(MySQL访问、连接池原理与简易网站数据流动)
数据库·学习·mysql·mysql访问·连接池原理
wefg15 小时前
【MySQL】索引(索引底层原理/创建/查看/删除主键、普通、联合、前缀、全文索引)
数据库·mysql
芝士爱知识a11 小时前
AI 模拟面试怎么做:智蛙公考智能体多轮对话 + 实时追问的工程实现
面试·职场和发展
帅次12 小时前
Android 高级工程师面试:Java 基础知识 近1年高频追问 22 题
android·java·面试