readview在事务首次快照读时生成,决定可见性判断起点;rr级别复用同一readview实现可重复读,rc级别每次快照读新建readview导致不可重复读,select for update等当前读不走readview。

ReadView 的生成时机决定可见性判断起点
可见性不是全局统一的,而是每个事务在第一次执行快照读(如普通 SELECT)时,由 InnoDB 自动生成一个 ReadView。这个时间点至关重要:它锁定了当时所有活跃事务 ID 的快照(m_ids),也确定了 min_trx_id 和 max_trx_id 边界。
容易踩的坑:
- 在
REPEATABLE READ隔离级别下,ReadView只在事务首个快照读时创建,后续所有快照读复用同一个视图 —— 这就是“可重复读”的底层实现,但也是很多人困惑“为什么 update 后 select 还看不到新值”的根源 - 在
READ COMMITTED下,每次快照读都新建ReadView,所以能看到其他事务刚提交的新版本,但也因此出现不可重复读 -
SELECT FOR UPDATE、UPDATE、DELETE等语句不走ReadView判断,它们是当前读,直接读最新版本并加锁
可见性判断逻辑依赖三个关键条件
对某行数据版本链上的一个节点(其 DB_TRX_ID 为 id),InnoDB 用如下规则逐个判断是否对该事务可见:
假设当前 ReadView 中:m_up_limit_id 是最小活跃事务 ID,m_low_limit_id 是下一个将分配的事务 ID,m_creator_trx_id 是当前事务自己的 ID。
判断顺序为:
- 若
id 或 <code>id == m_creator_trx_id→ 可见(旧版本已提交,或就是本事务改的) - 若
id >= m_low_limit_id→ 不可见(该事务在ReadView创建后才启动) - 若
id落在中间区间,则查m_ids集合:id不在其中 → 可见;否则不可见
这个逻辑直接对应源码中 changes_visible() 函数的分支结构,不是“大概匹配”,而是严格按这三步短路判断。
Undo 日志版本链是可见性遍历的物理载体
DB_ROLL_PTR 指向的是一条单向链表,从最新版本(链头)往最老版本(链尾)延伸。InnoDB 总是从链头开始,对每个节点的 DB_TRX_ID 执行上述判断,直到找到第一个可见版本,或链结束返回空。
注意几个实际限制:
- 只有聚簇索引记录带
DB_TRX_ID和DB_ROLL_PTR;二级索引不存这些字段,所以SELECT ... WHERE secondary_col = ?会先查二级索引定位主键,再回表到聚簇索引做可见性判断 - 被标记为删除的记录(
DB_TRX_ID有效但记录头含删除标记)仍参与判断,只是最终结果为空行 - Purge 线程只清理对所有现存
ReadView都不可见的版本,所以长事务会阻碍旧版本回收,导致undo_log膨胀
隔离级别差异本质是 ReadView 生命周期不同
RC 和 RR 的可见性行为差异,不在于判断逻辑本身,而在于 ReadView 的复用策略:
-
READ COMMITTED:每次快照读都调用trx_assign_read_view()新建视图 → 视图更“新”,能感知到刚提交的事务 -
REPEATABLE READ:仅首次快照读调用trx_assign_read_view(),之后复用 → 视图“冻结”,即使其他事务反复提交,只要不在活跃集合里,就永远不可见 -
READ UNCOMMITTED:跳过ReadView,直接读最新版本 → 可能脏读 -
SERIALIZABLE:自动将普通SELECT转为SELECT LOCK IN SHARE MODE→ 退化为锁机制,MVCC 失效
真正容易被忽略的是:事务未执行任何快照读时,ReadView 就不会生成,哪怕事务已开启、已执行 UPDATE —— 此时后续第一个 SELECT 才触发视图创建,而它看到的“历史”其实从那一刻才开始算起。











