Skip to content
MySQL 存储引擎与执行细节 · 第 1 篇 / 共 4 篇
领域数据与中间件
专题MySQL 专题
当前序列MySQL 存储引擎与执行细节
阅读位置第 1 篇 / 共 4 篇当前专题第 3 个序列 / 共 7 个序列

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 update
  • select ... lock in share mode
  • update
  • delete

当前读要读最新版本,并且通常要配合锁,避免并发写冲突。

一句话记忆:

  • 一致性读看版本
  • 当前读看当前值并加锁

数据版本是怎么保存的

InnoDB 每一行记录里会有几个关键隐藏信息:

  • trx_id: 最近一次修改该行的事务 ID
  • roll_pointer: 指向 undo log 的指针

当一行被更新时,不是简单原地覆盖,而是:

  1. 新值写到当前记录
  2. 旧值写入 undo log
  3. 当前行通过 roll_pointer 串起历史版本

这样,后来的事务如果发现“当前版本对我不可见”,就可以顺着 undo log 往回找旧版本。

Read View 到底是什么

Read View 可以理解成事务在某个时刻拍下的一张“活跃事务快照”。
它最核心的作用是判断一个版本是否对当前事务可见。

判断时会看几个边界值,常见记法可以理解成:

  • 当前系统已分配的最小活跃事务 ID
  • 当前系统已分配的最大事务 ID
  • 当前活跃事务列表

某个版本上的 trx_id 是否可见,大致按下面逻辑判断:

  1. 如果版本事务 ID 小于最小活跃事务 ID,说明它早就提交了,可见
  2. 如果版本事务 ID 大于等于最大事务 ID,说明它在 Read View 之后才产生,不可见
  3. 如果它落在中间区间,还要看它是否在活跃事务列表中
  4. 如果在活跃列表里,说明它拍快照时还没提交,不可见
  5. 如果不在活跃列表里,说明它拍快照时已经提交,可见

你不用死背字段名,但一定要明白:

Read View 并不是“最新值视图”,而是“可见性规则”。

不同隔离级别下,Read View 什么时候生成

Read Committed

每次一致性读都会生成新的 Read View。
所以同一个事务里,两次普通查询可能看到不同结果。

Repeatable Read

第一次一致性读时生成 Read View,后续复用同一个视图。
这就是为什么它能避免普通查询层面的不可重复读。

这也是 InnoDB 默认隔离级别是 Repeatable Read 的重要原因。

为什么说 RR 下也不是“完全没有幻读”

很多人背结论时容易简单化。

在 InnoDB 中:

  • 普通一致性读,大多数情况下通过 MVCC 避免了“读到新插入记录”带来的幻读体验
  • 当前读场景下,仍然需要靠间隙锁、临键锁来处理真正的并发插入问题

所以不能简单理解成“RR 完全不可能幻读”,而是要区分:

  • 普通快照读
  • 加锁当前读

一条最常见的业务链路

假设表里有一条记录余额为 100。

  1. 事务 T1 开启,做普通 select
  2. 事务 T2 把余额改成 80,并提交
  3. 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 可见性判断 -> 隔离级别决定视图生成时机

把这条链看顺之后,很多事务现象都会自然解释出来。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 顺着当前主线继续读隔离级别和幻读问题怎么放到 InnoDB 里看适合把 MVCC、索引、排序聚合和日志系统放在一起深入看。MySQL 专题 · MySQL 存储引擎与执行细节同一序列 · 顺着当前主线继续读覆盖索引和最左前缀原则怎么真正用起来适合把 MVCC、索引、排序聚合和日志系统放在一起深入看。MySQL 专题 · MySQL 存储引擎与执行细节同专题其他序列 · MySQL 变更与运维治理大表在线 DDL 怎么做更稳适合把计数、日志、在线变更和归档清理放在一起看。MySQL 专题 · MySQL 变更与运维治理同专题其他序列 · MySQL 变更治理与数据生命周期大事务为什么会拖垮数据库适合把大事务、DDL、回填、慢 SQL 治理和归档分层放进一条持续治理主线上看。MySQL 专题 · MySQL 变更治理与数据生命周期跨专题关联 · 同场景:基础学习@Conditional 系列注解怎么配合使用适合把自动装配、条件装配、Profile 和循环依赖放回 Spring Boot 启动过程里理解。Spring 专题 · Spring Boot 启动、装配与配置跨专题关联 · 同场景:基础学习保留策略和日志压缩适合哪些场景适合把副本机制、acks、批量压缩和日志组织放在一条 Kafka 投递主线上理解。Kafka 专题 · Kafka 投递链路与日志存储机制
继续阅读MySQL 存储引擎与执行细节当前序列第 1 篇 / 共 4 篇当前专题第 3 个序列 / 共 7 个序列
往前看
上一序列MySQL 事务、锁与高可用从第 1 篇开始:MySQL 事务与锁
往后看
下一篇隔离级别和幻读问题怎么放到 InnoDB 里看继续当前序列下一章下一序列MySQL 变更与运维治理从第 1 篇开始:COUNT(*) 和 COUNT(列) 到底差在哪

把零散经验整理成可查、可复用、可持续更新的企业级知识门户