readview在事务首次快照读时创建:rc级每次select新建,rr级仅首次创建后复用;它包含m_ids、min_trx_id、max_trx_id和creator_trx_id四个字段,用于基于版本链和undo log判断数据可见性。

ReadView 是什么时候创建的,取决于隔离级别
ReadView 不是全局唯一的,也不是每次查询都新建——它的生成时机由事务隔离级别硬性决定:
-
READ COMMITTED:每次执行普通SELECT(快照读)前,都会新建一个ReadView,所以能看到其他已提交事务的最新修改,导致“不可重复读” -
REPEATABLE READ:仅在事务**第一次执行快照读时**创建ReadView,之后整个事务生命周期内复用它,因此同一事务中多次SELECT结果一致 -
READ UNCOMMITTED:不使用ReadView,直接读取最新行记录,可能看到未提交数据 -
SERIALIZABLE:退化为加锁读,ReadView机制被绕过
容易踩的坑:误以为 REPEATABLE READ 下所有读都“绝对一致”——其实 SELECT ... FOR UPDATE 是当前读,不走 ReadView,会看到别的事务刚提交的版本。
可见性判断靠四个字段,不是简单比大小
ReadView 内部有四个关键字段:m_ids(活跃事务 ID 列表)、min_trx_id(列表最小值)、max_trx_id(下一个事务 ID)、creator_trx_id(当前事务 ID)。判断某行是否可见时,按顺序检查其 DB_TRX_ID:
- 若
DB_TRX_ID → 该行由已提交事务写入,<strong>可见</strong> - 若
DB_TRX_ID >= max_trx_id→ 该行由将来事务写入,不可见 - 若
DB_TRX_ID == creator_trx_id→ 当前行就是本事务自己写的,可见 - 若
DB_TRX_ID在m_ids中 → 表示写该行的事务尚未提交,不可见 - 若
DB_TRX_ID不在m_ids中,且min_trx_id → 说明该事务已提交,<strong>可见</strong>
注意:m_ids 是创建 ReadView 那一瞬所有未提交事务的 ID 快照,不是实时更新的。长事务会把大量旧 undo log 拖住不释放,直接影响 MVCC 空间回收。
版本链怎么连,靠的是 DB_ROLL_PTR
InnoDB 每行记录末尾的隐藏字段 DB_ROLL_PTR 是个 7 字节指针,指向 undo log 中上一个版本的物理位置。当事务更新一行时:
- 原记录不变,
DB_TRX_ID被更新为当前事务 ID -
DB_ROLL_PTR指向新生成的undo log记录(含旧值 + 事务 ID) - 新
undo log记录又通过自己的回滚指针连向前一个版本,形成单向链表
执行快照读时,InnoDB 从聚簇索引最新行开始,顺着 DB_ROLL_PTR 往前遍历版本链,对每个版本用当前 ReadView 做可见性判断,直到找到第一个“可见”的版本返回。这个过程不加锁、不阻塞,但链越长,遍历开销越大。
快照读和当前读混用时行为差异极大
很多人困惑:“为什么我在 RR 级别下,UPDATE 后再 SELECT 却看不到刚改的值?”答案就在读类型切换:
-
SELECT * FROM t WHERE id = 1→ 快照读,走ReadView,读的是事务启动时的快照 -
SELECT * FROM t WHERE id = 1 FOR UPDATE→ 当前读,跳过ReadView,直接读最新行并加锁 -
UPDATE t SET x = x + 1 WHERE id = 1→ 底层先做一次当前读(获取最新值),再更新生成新版本
关键点:同一个事务里,快照读和当前读看到的数据版本可以完全不同。这并非 bug,而是 MVCC 的设计契约——你必须清楚每条 SQL 触发的是哪一类读,否则并发逻辑极易出错。











