📖 本篇简介:MVCC(多版本并发控制)是 InnoDB 高并发的基石,也是中高级面试的分水岭。本篇用 16 道高频题讲清 MVCC 的三大组件——隐藏字段、undo log 版本链、ReadView,把"快照读为什么不加锁""RC 和 RR 为什么可见性不同""长事务为什么让 undo 暴涨"一次讲透。学完本篇,你能手绘一条记录的版本链,并说出一个事务读到某版本的完整判定过程。前置知识:第 3、4、5 章事务、隔离级别、锁;本系列示例基于 MySQL 8.0。
目录
- 1 MVCC 高频考点地图
- 题目 1什么是 MVCC它解决什么问题
- 题目 2MVCC 的三大组件是什么
- 题目 3InnoDB 行的隐藏列有哪些分别干什么
- 题目 4undo log 版本链是怎么串起来的
- 题目 5ReadView 是什么结构包含哪些字段
- 题目 6ReadView 的可见性判定规则是什么
- 题目 7RC 和 RR 的 ReadView 生成时机有什么不同
- 题目 8MVCC 怎么做到读不阻塞写写不阻塞读
- 题目 9快照读和当前读在 MVCC 里的角色
- 题目 10MVCC 找历史版本的完整过程
- 题目 11MVCC 的代价是什么
- 题目 12聚簇索引和二级索引的 MVCC 有何不同
- 题目 13为什么长事务会让 undo log 暴涨
- 题目 14MVCC 能完全代替加锁吗
- 题目 1580 的 MVCC 相比 57 有什么改进
- 题目 16怎么实验验证 MVCC 在工作
- MVCC 速记口诀
- 常见坑与注意事项
- 复习清单
- 练习题
1 MVCC 高频考点地图
| 难度 | 典型问题 | 本篇对应 |
|---|---|---|
| 入门 | MVCC 是什么?解决什么? | 题目 1 |
| 基础 | 三大组件?ReadView 结构? | 题目 2、3、5 |
| 进阶 | 可见性判定?版本链? | 题目 6、10 |
| 实战 | undo 暴涨?快照读 vs 当前读? | 题目 9、13 |
一条主线记住:MVCC = 隐藏列记版本 + undo log 串历史 + ReadView 挑可见版本。读不加锁,靠"挑历史版本"实现并发。
题目 1:什么是 MVCC?它解决什么问题?
考点
- Multi-Version Concurrency Control;
- 多版本 + 快照读;
- 读写并发不互斥。
参考答案
- MVCC(Multi-Version Concurrency Control,多版本并发控制):InnoDB 通过为每行数据维护多个历史版本,让读操作读到"某一时刻的快照",而不用加锁等待写;
- 解决的核心问题:
1. 读写并发:读不阻塞写、写不阻塞读,大幅提升吞吐;
2. 实现 RC/RR 隔离级别:靠 ReadView 决定看到哪个版本;
- 没有 MVCC 的数据库(如 MyISAM),读写互相阻塞,并发上不去。
-- MVCC 带来的效果:会话 A 在改,会话 B 读不阻塞
-- A:
BEGIN; UPDATE account SET balance=1 WHERE id=1; -- 未提交
-- B(普通 SELECT,不加锁):
SELECT balance FROM account WHERE id=1; -- 读到旧值,不被 A 阻塞
追问
- 问:MVCC 是所有存储引擎都有吗?答:InnoDB 有;MyISAM/Memory 没有。
- 问:MVCC 和快照隔离什么关系?答:MVCC 是实现手段,快照隔离是它提供的隔离级别语义。
题目 2:MVCC 的三大组件是什么?
考点
- 隐藏字段;
- undo log 版本链;
- ReadView。
参考答案
MVCC 由三部分组成:
1. 隐藏字段:每行自带 DB_TRX_ID(最后修改的事务 ID)、DB_ROLL_PTR(指向 undo log 的指针);
2. undo log 版本链:每次 UPDATE 都把旧值写到 undo log,用 roll_ptr 串成链表;
3. ReadView:事务读数据时生成的"快照",记录当时活跃事务列表,用来判断哪个版本可见。
- 一句话:隐藏字段记当前版本,undo log 串历史,ReadView 挑可见版本。
追问
- 问:DELETE 也进版本链吗?答:InnoDB 的 DELETE 实际是打删除标记(update 一个删除位),也写 undo,所以能回滚和 MVCC。
- 问:INSERT 的历史版本?答:INSERT 的 undo 在事务提交后即可purge(删除时才用得到),不像 UPDATE 要长期保留。
题目 3:InnoDB 行的隐藏列有哪些?分别干什么?
考点
- DB_TRX_ID:6 字节,最后修改事务 ID;
- DB_ROLL_PTR:7 字节,回滚指针;
- DB_ROW_ID:6 字节,无主键时用。
参考答案
InnoDB 每行有三个隐藏列(用户看不见):
| 隐藏列 | 大小 | 作用 |
|---|---|---|
| DB_TRX_ID | 6 字节 | 最后一次 INSERT/UPDATE 这行的事务 ID |
| DB_ROLL_PTR | 7 字节 | 回滚指针,指向 undo log 中这行的上一个版本 |
| DB_ROW_ID | 6 字节 | 没有主键也没有唯一索引时,InnoDB 自动生成的行 ID |
DB_TRX_ID是 MVCC 判断"这个版本是谁改的"的依据;DB_ROLL_PTR把当前行和 undo 里的旧版本串起来,形成版本链;- 有主键时 DB_ROW_ID 不出现(主键就是聚簇索引)。
-- 普通 SELECT 看不到隐藏列,但可以用 innodb_row_ops 间接观察
-- 实际工程中不需要手动管,理解原理即可
SELECT * FROM information_schema.INNODB_TRX; -- trx_id 即事务 ID,对应 DB_TRX_ID
追问
- 问:事务 ID 是全局递增的吗?答:是,InnoDB 全局 trx_sys 分配,单调递增,所以 trx_id 大小可以代表"时间先后"。
- 问:DB_TRX_ID 会不会溢出?答:6 字节理论上很大,实践中不会;InnoDB 有 wrap-around 处理。
题目 4:undo log 版本链是怎么串起来的?
考点
- 每次 UPDATE 写一条 undo;
- roll_ptr 指向前一版本;
- 链头是最新版本。
参考答案
- 一条记录被 UPDATE 多次后:
1. 当前行的 DB_TRX_ID = 最新事务 ID,DB_ROLL_PTR 指向 undo log 里的"上一版本";
2. undo log 里那个旧版本也有自己的 DB_TRX_ID 和 DB_ROLL_PTR,再指向更早版本;
3. 这样串成一条从新到旧的版本链;
- 读数据时,从当前行开始沿链找,找第一个"对当前 ReadView 可见"的版本。
当前行 (trx_id=300, roll_ptr→undo B)
undo B (trx_id=200, roll_ptr→undo A)
undo A (trx_id=100, roll_ptr=null)
- 事务 trx_id=250 的 ReadView 来读:当前行 trx_id=300 不可见 → 沿链到 B(trx_id=200,可见)→ 读到 B。
追问
- 问:版本链会无限长吗?答:不会,purge 线程把"没有任何 ReadView 需要的旧版本"清理掉。
- 问:INSERT 的 undo 在链上吗?答:INSERT 的 undo 只在事务回滚时用,提交后就标记可 purge,不在 MVCC 版本链里长期挂着。
题目 5:ReadView 是什么结构?包含哪些字段?
考点
- m_ids:生成时活跃事务 ID 列表;
- min_trx_id / max_trx_id;
- creator_trx_id。
参考答案
ReadView 是事务读数据时拍的"快照",核心字段:
| 字段 | 含义 |
|---|---|
| m_ids | 生成 ReadView 时,当前活跃(未提交)事务的 ID 列表 |
| min_trx_id | m_ids 里的最小值(最早活跃事务) |
| max_trx_id | 生成 ReadView 时 InnoDB 即将分配的下一个事务 ID |
| creator_trx_id | 生成这个 ReadView 的事务自己的 ID |
- 判断一个版本对当前事务可不可见,就看它的
DB_TRX_ID落在哪个区间。
追问
- 问:max_trx_id 是"当前最大活跃事务 ID"吗?答:不是,是"下一个将分配的 ID",即所有 ≥ max_trx_id 的事务都发生在 ReadView 之后。
- 问:ReadView 占内存大吗?答:很小,就是一个事务 ID 列表,8.0 优化后更轻。
题目 6:ReadView 的可见性判定规则是什么?
考点
- 四个判断分支;
- 沿版本链找第一个可见版本。
参考答案
对某版本的 trx_id,判定:
1. trx_id == creator_trx_id:自己改的,可见;
2. trx_id < min_trx_id:生成 ReadView 前已提交,可见;
3. trx_id >= max_trx_id:生成 ReadView 后才开始的事务,不可见;
4. min_trx_id ≤ trx_id < max_trx_id:看 trx_id 在不在 m_ids 里——在(当时活跃)→ 不可见;不在(当时已提交)→ 可见。
- 不可见就沿
DB_ROLL_PTR找 undo log 里的上一个版本,重复判断,直到找到可见版本。
trx_id=300 版本:
creator=250 → 自己,可见
min=200, max=400, m_ids={200,300}
- trx_id=100 < min=200 → 可见(老事务已提交)
- trx_id=250 == creator → 可见
- trx_id=350 ≥ max=400?不,350<400 → 在 m_ids?不在 → 可见(已提交)
- trx_id=300 在 m_ids → 不可见,沿 roll_ptr 找上一版本
追问
- 问:如果链上所有版本都不可见怎么办?答:不会,链头前的 INSERT 版本一定可见(它在 ReadView 之前已提交)。
- 问:删除标记的行怎么处理?答:最后一个版本是"删除标记",不可见时查询返回空。
题目 7:RC 和 RR 的 ReadView 生成时机有什么不同?
考点
- RC 每条 SELECT 新建 ReadView;
- RR 第一次 SELECT 建一次复用。
参考答案
- RC(READ COMMITTED):每执行一条普通 SELECT 都生成新的 ReadView,所以能看到别的事务最新提交;
- RR(REPEATABLE READ):事务启动后第一次普通 SELECT 时生成 ReadView,整个事务复用同一个,之后都看不到新提交;
- 这就是 RC"不可重复读"、RR"可重复读"的根因。
-- RR:ReadView 在第一次 SELECT 时固定
BEGIN;
SELECT ...; -- ← 此刻生成 ReadView
SELECT ...; -- 复用,结果相同
COMMIT;
-- RC:每条 SELECT 新 ReadView
BEGIN;
SELECT ...; -- 新 ReadView
SELECT ...; -- 又一个新 ReadView,可能看到新提交
追问
- 问:BEGIN 立刻生成 ReadView 吗?答:不,第一次普通 SELECT 才生成;
START TRANSACTION WITH CONSISTENT SNAPSHOT才立刻生成。 - 问:当前读走 ReadView 吗?答:不走,当前读读最新版本 + 加锁,不受 ReadView 限制。
题目 8:MVCC 怎么做到"读不阻塞写、写不阻塞读"?
考点
- 读走快照不加锁;
- 写加行锁但不影响读旧版本;
- 吞吐提升。
参考答案
- 读(普通 SELECT):走 MVCC 读快照,不加任何锁,所以不阻塞写;
- 写(UPDATE/DELETE):对当前行加 X 锁,但其他事务的普通 SELECT 仍能读 undo log 里的旧版本,不阻塞读;
- 对比没有 MVCC 的系统:读写互相阻塞,并发上不去;
- 这就是 InnoDB 高并发的核心。
追问
- 问:写会阻塞写吗?答:会,同一行两个 UPDATE 要排队,靠 X 锁。
- 问:当前读(FOR UPDATE)还享受 MVCC 吗?答:不享受,它读最新版本并加锁,属于"读也要锁"。
题目 9:快照读和当前读在 MVCC 里的角色?
考点
- 快照读 = MVCC;
- 当前读 = 最新版本 + 锁;
- 混用的坑。
参考答案
- 快照读:普通
SELECT,走 MVCC 读 ReadView 选定的版本,不加锁; - 当前读:
SELECT ... FOR UPDATE/FOR SHARE、UPDATE、DELETE,读最新已提交版本并加锁,不走 ReadView; - 两者并存是为了业务灵活性:查询用快照读性能好,"读了就要改"用当前读保证不丢更新;
- 混用会出现"逻辑幻读":事务内先快照读看不到新行,再当前读看到了(见第 4 章题目 9)。
BEGIN;
SELECT balance FROM account WHERE id=1; -- 快照读,100(MVCC)
-- 别的事务改成 50 提交
SELECT balance FROM account WHERE id=1; -- 快照读,还是 100
SELECT balance FROM account WHERE id=1 FOR UPDATE; -- 当前读,50(最新)
COMMIT;
追问
- 问:UPDATE 是当前读,那它看到的旧版本会被回滚吗?答:UPDATE 基于最新值改,会写新 undo;事务回滚时用 undo 反向恢复。
- 问:快照读能看到自己事务所做的修改吗?答:能,ReadView 把 creator_trx_id 算可见,这就是"写后读一致"。
题目 10:MVCC 找历史版本的完整过程?
考点
- 沿版本链;
- 每步用 ReadView 判定;
- 找到即停。
参考答案
完整过程:
1. 从聚簇索引读到当前行,取它的 DB_TRX_ID;
2. 用 ReadView 判定该版本可见性:
- 可见 → 返回这一行;
- 不可见 → 顺着
DB_ROLL_PTR找到 undo log 里的上一个版本;
3. 对上个版本重复步骤 2,直到找到第一个可见版本;
4. 如果链上最后是"删除标记"且不可见,返回该行不存在。
- 整个过程不需要加锁,代价是可能要沿链多读几页 undo log。
追问
- 问:版本链很长会不会慢?答:长事务下版本链可能很长,读旧版本要多跳几次 undo;这也是长事务危害之一。
- 问:purge 后旧版本没了怎么办?答:purge 只清"没有任何 ReadView 需要的版本",正在活跃的 ReadView 对应的版本不会被清。
题目 11:MVCC 的代价是什么?
考点
- 存储开销:undo log 占空间;
- CPU:可见性判断;
- 长事务拖 purge。
参考答案
- 存储开销:每行多 6+7 字节隐藏列,undo log 占用大量表空间;
- CPU 开销:每次快照读都要沿版本链做可见性判断;
- 长事务放大问题:长事务是"最旧活跃 ReadView",purge 不能清它之前的 undo,导致 undo 暴涨、ibdata 膨胀;
- 收益(高并发)远大于代价,这是 InnoDB 默认架构。
-- 观察 undo 堆积:History list length
SHOW ENGINE INNODB STATUS\G
-- 关注 TRANSACTIONS 段的 History list length,持续上涨 = 有长事务
追问
- 问:undo 暴涨磁盘满了怎么办?答:找长事务 KILL 掉,purge 会追上;8.0 独立 undo 表空间可自动 truncate。
- 问:MVCC 能关掉吗?答:不能,它是 InnoDB 核心机制;隔离级别降到 RU 只是 ReadView 简化,不是关 MVCC。
题目 12:聚簇索引和二级索引的 MVCC 有何不同?
考点
- 二级索引叶子存主键;
- 二级索引的 undo 链通过 roll_ptr 回聚簇索引;
- 回表时再走 MVCC。
参考答案
- 聚簇索引:叶子直接是整行,有完整隐藏列和 roll_ptr,MVCC 版本链直接在聚簇索引上串;
- 二级索引:叶子存主键值,没有自己的 roll_ptr。二级索引记录的"旧版本"怎么找?
1. 二级索引页里也有一个隐藏的 trx_id 和 roll_ptr(8.0 叫 secondary index undo log);
2. 找不到对应版本时,先拿到主键值,回聚簇索引走完整版本链;
- 所以二级索引的 MVCC 比聚簇索引复杂,开销也略大。
-- 二级索引查找后回表,回表时再做 MVCC 判定
-- SELECT * FROM t WHERE name='tom';
-- 1. 二级索引 idx_name 找到 name='tom' 的主键 id=10
-- 2. 用 id=10 回聚簇索引,沿版本链找可见版本
追问
- 问:覆盖索引还需要回表做 MVCC 吗?答:覆盖索引要的列都在二级索引里,不回聚簇索引,直接在二级索引上做可见性判断,性能更好。
- 问:8.0 对二级索引 MVCC 有优化吗?答:8.0 改进了 secondary index 的 undo 处理,减少回表。
题目 13:为什么长事务会让 undo log 暴涨?
考点
- purge 水位线 = 最旧活跃 ReadView;
- 长事务拖住水位;
- 解决:kill 长事务。
参考答案
- purge 线程清理 undo log 的前提:这个旧版本对所有活跃事务都不可见了;
- "最旧活跃事务"决定了水位线——只要有一个长事务还在跑,它之前产生的所有 undo 都不能清;
- 结果:undo log 越堆越多,表空间膨胀,查询变慢;
- 治理:监控
INNODB_TRX的trx_started,跑超过 1 分钟的事务重点排查。
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_sec,
trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started; -- 最老的排在前面
追问
- 问:只读事务也算长事务吗?答:算,它也有 ReadView,拖住 purge;8.0 对只读事务有优化但仍要控时长。
- 问:怎么定位是谁拖住的?答:找 trx_started 最早的那个事务,看 trx_mysql_thread_id,KILL 掉。
题目 14:MVCC 能完全代替加锁吗?
考点
- 快照读无锁;
- 当前读/写必须加锁;
- 一致性写需要锁。
参考答案
- 不能完全代替:
1. 读:普通 SELECT 走 MVCC 无锁,够了;
2. 写:UPDATE/DELETE 必须加 X 锁,否则两个事务并发写同一行会丢更新;
3. 读了就要改:必须 SELECT ... FOR UPDATE 当前读 + 加锁,否则 MVCC 读到旧值改上去会覆盖别人;
- MVCC 解决的是"读写并发",写写并发还是要靠锁。
-- 反例:靠 MVCC 读然后改,丢更新
-- A:
SELECT balance FROM account WHERE id=1; -- 100(MVCC)
-- B:
SELECT balance FROM account WHERE id=1; -- 100
UPDATE account SET balance=50 WHERE id=1; -- B 提交
-- A:
UPDATE account SET balance=balance-10 WHERE id=1; -- 基于旧值,覆盖 B 的修改
-- 正解:SELECT ... FOR UPDATE 加锁
追问
- 问:那为什么不所有读都 FOR UPDATE?答:那样读写互斥,并发暴跌,MVCC 的意义就没了。
- 问:CAS 乐观锁算 MVCC 吗?答:不算,它是应用层版本号校验,不加 DB 锁,但思想类似"多版本"。
题目 15:8.0 的 MVCC 相比 5.7 有什么改进?
考点
- ReadView 性能优化;
- 历史列表 purge 更积极;
- 事务 ID 缓存。
参考答案
- 8.0 对 MVCC 的改进:
1. ReadView 创建开销降低:只读事务的 ReadView 不再复制整个活跃事务列表;
2. purge 更并行:多线程 purge,undo 清理更快;
3. transaction ID 改进:避免 64 位 trx_id 回绕问题;
4. 变更缓冲(change buffer)优化:二级索引写性能提升;
- 核心机制(隐藏列 + undo 链 + ReadView)和 5.7 一致,面试不用背具体参数。
追问
- 问:8.0 默认隔离级别变了吗?答:还是 RR,没变。
- 问:8.0 的 undo 能自动 truncate 吗?答:可以,
innodb_undo_log_truncate=ON,独立 undo 表空间自动收缩,5.7 要手工。
题目 16:怎么实验验证 MVCC 在工作?
考点
- 两个会话对比;
- 普通 SELECT 看不到未提交;
- FOR UPDATE 看得到最新。
参考答案
用两个会话验证(本示例基于 MySQL 8.0,先 SET SESSION transaction_isolation='REPEATABLE-READ'):
-- 会话 A:
BEGIN;
UPDATE account SET balance=1 WHERE id=1; -- 未提交,不 COMMIT
-- 会话 B(另一连接):
SELECT balance FROM account WHERE id=1; -- 看到旧值(MVCC 读 undo 快照),不阻塞
-- 会话 A:COMMIT;
-- 会话 B 再普通 SELECT:RR 下仍看到旧值(ReadView 固定)
-- 会话 B:SELECT balance FROM account WHERE id=1 FOR UPDATE; -- 看到最新 1
- 现象解释:B 的普通 SELECT 走 MVCC,A 未提交时读旧版本;A 提交后,B 在 RR 下 ReadView 固定仍读旧值;FOR UPDATE 当前读才看到最新。
追问
- 问:怎么看版本链?答:工程上不直接看,理解原理即可;
innodb_ruby等工具可解析页结构。 - 问:MVCC 对性能影响多大?答:读性能接近 MyISAM,写性能略低于纯锁,但综合并发远好。
MVCC 速记口诀
| 组件 | 一句话 |
|---|---|
| 隐藏列 | trx_id 记谁改的,roll_ptr 指向上一版本 |
| undo 版本链 | 新 → 旧,沿 roll_ptr 找历史 |
| ReadView | 拍快照,记当时活跃事务列表 |
| 可见性 | 自己改的可见;< min 可见;≥ max 不可见;中间看在不在活跃列表 |
| RC 生成时机 | 每条 SELECT 新建 |
| RR 生成时机 | 第一次 SELECT 建一次复用 |
| 快照读 | 普通 SELECT,走 MVCC 不加锁 |
| 当前读 | FOR UPDATE/UPDATE,读最新加锁 |
| purge 水位 | 最旧活跃事务,长事务拖住不释放 |
常见坑与注意事项
1. 以为 MVCC 能防写写并发:不能,写还是要加 X 锁。
2. 以为 RR 下所有读都一致:当前读读最新,混用会"逻辑幻读"。
3. 长事务不自知:RR 下一个大 SELECT 跑 10 分钟,undo 涨几个 G。
4. undo 暴涨不敢动:找到最老活跃事务 KILL 掉,purge 会追上;不要直接清 ibdata。
5. 以为覆盖索引不做 MVCC:覆盖索引在二级索引上做可见性判断,不回聚簇索引。
6. RC 下以为 ReadView 一次就好:RC 每条 SELECT 新建,这是它和 RR 的本质区别。
7. 把 MVCC 和乐观锁混为一谈:MVCC 是数据库多版本,乐观锁是应用层 version 字段,思想类似但实现不同。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| MVCC 定义 | 多版本 + 快照读,读写不互斥 | 说"加锁读" |
| 三大组件 | 隐藏列 + undo 链 + ReadView | 漏一个 |
| 隐藏列 | trx_id、roll_ptr、row_id | 大小记不清没关系,作用要讲对 |
| 版本链 | roll_ptr 串 undo,新→旧 | 方向说反 |
| ReadView | m_ids/min/max/creator | max_trx_id 是"下一个将分配" |
| 可见性 | 自己/已提交可见,活跃/未来不可见 | 判定分支漏 |
| RC vs RR | ReadView 生成时机不同 | 答成"事务开始生成" |
| 快照/当前读 | 普通 SELECT 走 MVCC,FOR UPDATE 不走 | 混淆 |
| undo 暴涨 | 长事务拖住 purge 水位 | 只读事务不算 |
| MVCC vs 锁 | 读无锁,写仍要锁 | 以为 MVCC 替代锁 |
练习题
练 1:事务 T(trx_id=500)生成 ReadView,当时活跃事务为 {200, 300},下一个将分配 trx_id=600。某记录当前版本 trx_id=400,它对 T 可见吗?如果 trx_id=250 呢?
<details>
<summary>参考答案</summary>
- trx_id=400:min=200, max=600,200≤400<600,看是否在 m_ids={200,300}——不在,说明 400 在 ReadView 生成前已提交,可见。
- trx_id=250:同样在 [200,600) 区间,不在 m_ids,已提交,可见。
- 如果 trx_id=300(在 m_ids),不可见,要沿 roll_ptr 找更早版本。
</details>
练 2:为什么 mysqldump --single-transaction 导大库时不能有长事务跑在业务上?
<details>
<summary>参考答案</summary>
--single-transaction 在 RR 下用一个长 ReadView 保证导出一致性;如果业务上还有更长的事务,purge 水位被业务长事务拖住,undo 在 dump 期间暴涨。反过来 dump 本身也是长事务,业务上跑大 UPDATE 会让 undo 链很长。所以大库导出要在低峰期、业务低负载时做。
</details>
练 3:RR 下事务 A 第一次 SELECT 读到 balance=100;事务 B 把它改成 50 并提交;事务 A 第二次普通 SELECT 读到多少?改成 SELECT ... FOR UPDATE 又读到多少?为什么?
<details>
<summary>参考答案</summary>
第二次普通 SELECT 读到 100:A 的 ReadView 在第一次 SELECT 时固定,B 的 trx_id 在 m_ids 生成时未活跃或不在可见范围,沿版本链读到 trx_id 更早的 100 版本。
改成 FOR UPDATE 是当前读,读最新已提交值 50,并加 X 锁。
这就是 MVCC 快照读与当前读的差异。
</details>
下一篇预告:第 7 章 InnoDB架构面试精讲
文章回复
0 条公开回复