一、MVCC 是什么
MVCC 是一种"多版本 + 快照读"的并发控制机制,用来实现隔离性,并通过不同的 ReadView 策略支撑不同隔离级别,同时保证读写不阻塞。
二、MVCC的实现
1、隐藏字段(每行记录的"身份证")
InnoDB 在每行记录上,除了你定义的字段,还偷偷加了几个隐藏字段:
而与 MVCC 相关的字段只有前2项
| 隐藏字段 | 含义 | 作用 |
|---|---|---|
| DB_TRX_ID | 最后一次修改该行的事务 ID | 判断版本可见性 |
| DB_ROLL_PTR | 回滚指针,指向 undo log 中的上一个版本 | 串联版本链 |
2、Undo Log版本链(历史版本仓库)
每次修改数据,旧值都会被写进 undo log ,并通过 DB_ROLL_PTR(回滚指针) 串成一条版本链。

特点:
-
链首是最新版本。
-
越往后越旧。
-
每个版本都带
trx_id,用于可见性判断。
3、ReadView(可见性判断容器)
这里先说一下快照读 和当前读:
快照读是什么? = 读历史版本(快照),不是读最新 = 普通 SELECT = 不加锁 = 靠 MVCC 实现(ReadView + undo log 版本链) 对比: 快照读 → 读历史、不加锁、并发高 当前读 → 读最新、加锁、保证强一致
ReadView(读视图)是事务在执行快照读时生成的一个对象,里面记录了"生成这一刻"数据库的活跃事务信息,用来判断版本链上的哪个版本对当前事务可见。
ReadView 的四个字段
| 字段 | 含义 |
|---|---|
| m_ids | 生成 ReadView 时,当前**活跃(未提交)**的事务 ID 列表 |
| min_trx_id | m_ids 中的最小值 |
| max_trx_id | 下一个将要分配的事务 ID(当前最大 +1) |
| creator_trx_id | 创建该 ReadView 的事务 ID |
4、可见性判断算法
规则①:自己改的,可见
事务 A 改了数据,虽然还没提交
但 A 自己当然要能看到自己的修改
→ creator_trx_id == trx_id → 可见
规则②:早于 ReadView 生成时刻的,可见
如果 trx_id < min_trx_id
说明这个事务在 ReadView 生成之前就已经"不在活跃列表"了
→ 意味着它早就提交了 → 可见
规则③:晚于 ReadView 生成时刻的,不可见
如果 trx_id >= max_trx_id
说明这个事务是在 ReadView 生成之后才开启的
→ 快照不该看到未来的事务 → 不可见
规则④:介于两者之间的,看是否活跃
在 min 和 max 之间的,可能是:
已提交的
还活跃的
怎么区分?看它是否在 m_ids 里:
在 m_ids → 当时还活跃 → 不可见
不在 m_ids → 当时已提交 → 可见
不可见怎么办
当前版本不可见
↓
沿 DB_ROLL_PTR(回滚指针) 找上一个版本
↓
再判断,直到找到可见版本
↓
若都不可见,说明该行对当前事务"不存在"
三、隔离级别与MVCC的关系
| 隔离级别 | 实现方式 | 用 MVCC 吗 |
|---|---|---|
| READ UNCOMMITTED(读未提交) | 直接读最新,不加过滤 | ❌ |
| READ COMMITTED(读已提交) | MVCC(每次新建 ReadView) | ✅ |
| REPEATABLE READ(不可重复读) | MVCC(复用 ReadView) | ✅ |
| SERIALIZABLE(串行化) | 加锁,串行执行 | ❌ |
四、MVCC 如何实现 RC (读未提交) 和 RR(不可重复读)
快照读时,会沿着版本链逐个判断,一旦某个版本满足当前 ReadView 的可见性规则,就读这个版本并返回。
MVCC 靠"同一个版本链 + 不同的 ReadView 生成时机",实现了 RC 和 RR 的差异。
| 维度 | RC(读已提交) | RR(可重复读) |
|---|---|---|
| ReadView 生成时机 | 每次 SELECT 都新建 | 事务首次 SELECT 生成,之后复用 |
| 快照范围 | 每次读都是"最新快照" | 全程一个"固定快照" |
| 能否读到别人提交 | ✅ 能(提交后下次读就能看到) | ❌ 不能(快照固定) |
| 解决的问题 | 脏读 | 脏读 + 不可重复读 |
五、事务中的隔离性是如何保证的
事务的隔离性是由锁和 MVCC 实现的。
其中 MVCC 的意思是多版本并发控制 ,指维护一个数据的多个版本,使得读写操作没有冲突 。它的底层实现主要分为三个部分:隐藏字段、undo log 日志、ReadView 读视图。
隐藏字段 :在 MySQL 中给每个表都设置了隐藏字段,一个是 trx_id(事务 id),记录每一次操作的事务 id,是自增的;另一个是 roll_pointer(回滚指针),指向上一个版本的事务版本记录地址。
undo log :主要作用是记录回滚日志,存储老版本数据。在内部会形成一个版本链,在多个事务并行操作某一行记录时,记录不同事务修改数据的版本,通过 roll_pointer 指针形成一个链表。
ReadView :解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务 id,判断该访问哪个版本的数据。不同的隔离级别快照读是不一样的,最终的访问结果也不一样:
-
RC 隔离级别:每一次执行快照读时生成 ReadView。
-
RR 隔离级别:仅在事务中第一次执行快照读时生成 ReadView,后续复用。