Appearance
MVCC 和 Read View 怎么理解
很多人学 MVCC 的时候会卡在几个名词上:
- undo log
- 隐藏列
- Read View
- 一致性读
但真正落到项目里,你最需要回答的问题其实只有一个:
同样一行数据,为什么两个事务在同一时刻能看到不同版本?
先说结论
- MVCC 的目标是让“读不阻塞写、写不阻塞读”成为大多数普通事务的默认体验
- Read View 本质上是一张“当前事务可见版本边界表”
- 一致性读依赖 MVCC 和 Read View,当前读依赖锁
- 理解 MVCC 时,一定把
undo log、事务 ID、隔离级别一起看
MVCC 到底在解决什么
如果没有 MVCC,最直接的并发控制方式就是读写互相加锁。
这样虽然简单,但并发能力会很差。
MVCC 的核心思路是:
- 写操作产生数据的新版本
- 旧版本通过 undo log 保留
- 读操作根据自己的事务视图,决定看哪个版本
所以它本质上是“多版本并发控制”,不是“没有锁”,而是把大量普通读请求从锁竞争里解放出来。
先区分一致性读和当前读
这是理解 MySQL MVCC 的第一步。
一致性读
普通 select 在 InnoDB 下通常是一致性读。
它不会去读最新提交中的某个绝对瞬时值,而是读“对当前事务可见的版本”。
当前读
下面这些属于当前读:
select ... for updateselect ... lock in share modeupdatedelete
当前读要读最新版本,并且通常要配合锁,避免并发写冲突。
一句话记忆:
- 一致性读看版本
- 当前读看当前值并加锁
数据版本是怎么保存的
InnoDB 每一行记录里会有几个关键隐藏信息:
trx_id: 最近一次修改该行的事务 IDroll_pointer: 指向 undo log 的指针
当一行被更新时,不是简单原地覆盖,而是:
- 新值写到当前记录
- 旧值写入 undo log
- 当前行通过
roll_pointer串起历史版本
这样,后来的事务如果发现“当前版本对我不可见”,就可以顺着 undo log 往回找旧版本。
Read View 到底是什么
Read View 可以理解成事务在某个时刻拍下的一张“活跃事务快照”。
它最核心的作用是判断一个版本是否对当前事务可见。
判断时会看几个边界值,常见记法可以理解成:
- 当前系统已分配的最小活跃事务 ID
- 当前系统已分配的最大事务 ID
- 当前活跃事务列表
某个版本上的 trx_id 是否可见,大致按下面逻辑判断:
- 如果版本事务 ID 小于最小活跃事务 ID,说明它早就提交了,可见
- 如果版本事务 ID 大于等于最大事务 ID,说明它在 Read View 之后才产生,不可见
- 如果它落在中间区间,还要看它是否在活跃事务列表中
- 如果在活跃列表里,说明它拍快照时还没提交,不可见
- 如果不在活跃列表里,说明它拍快照时已经提交,可见
你不用死背字段名,但一定要明白:
Read View 并不是“最新值视图”,而是“可见性规则”。
不同隔离级别下,Read View 什么时候生成
Read Committed
每次一致性读都会生成新的 Read View。
所以同一个事务里,两次普通查询可能看到不同结果。
Repeatable Read
第一次一致性读时生成 Read View,后续复用同一个视图。
这就是为什么它能避免普通查询层面的不可重复读。
这也是 InnoDB 默认隔离级别是 Repeatable Read 的重要原因。
为什么说 RR 下也不是“完全没有幻读”
很多人背结论时容易简单化。
在 InnoDB 中:
- 普通一致性读,大多数情况下通过 MVCC 避免了“读到新插入记录”带来的幻读体验
- 当前读场景下,仍然需要靠间隙锁、临键锁来处理真正的并发插入问题
所以不能简单理解成“RR 完全不可能幻读”,而是要区分:
- 普通快照读
- 加锁当前读
一条最常见的业务链路
假设表里有一条记录余额为 100。
- 事务 T1 开启,做普通
select - 事务 T2 把余额改成 80,并提交
- T1 再次普通
select
如果隔离级别是 RR,T1 第二次大概率还会看到 100。
不是因为数据库没更新,而是因为它沿着自己的 Read View 看到了旧版本。
如果 T1 执行的是 select ... for update,那它看的就是当前值和锁语义,不再是老快照。
工程上最容易混淆的几个点
1. 把 MVCC 理解成“完全无锁”
错。
MVCC 主要服务于一致性读;写操作之间、当前读场景下,锁仍然非常重要。
2. 只会背 Read View,看不懂线上现象
线上看到“同一事务里两次 select 结果不一样”,你要先问:
- 隔离级别是什么
- 是普通 select 还是 for update
- Read View 是每次生成还是第一次复用
3. 忽略 undo log 的代价
长事务会拖住 undo 清理,导致历史版本回收变慢,进一步带来:
- undo 空间膨胀
- purge 跟不上
- 查询回旧版本成本升高
实战建议
- 避免长事务,尤其是“开事务后长时间不提交”
- 读多写少的链路尽量用普通一致性读,不要无意义上锁
- 排查并发一致性问题时,把隔离级别和 SQL 类型一起看
- 解释线上现象时,不要只说“MVCC 导致的”,要说清楚是哪一种读
总结
MVCC 的关键不是把概念背熟,而是建立这样一条图:
行记录当前版本 -> undo log 历史版本链 -> Read View 可见性判断 -> 隔离级别决定视图生成时机
把这条链看顺之后,很多事务现象都会自然解释出来。