MVCC:快照读与版本链

kp-017 · 事务与并发控制核心约 30 分钟已校对

前置知识点

前置:

相关知识点

相关:

学习进度:

一句话定义

多版本并发控制(MVCC,Multi-Version Concurrency Control)为每行数据保留多个历史版本,读者按事务快照挑选可见版本,从而实现「读写不互斥」——读不加锁、写不阻塞读。

为什么重要

前置知识

kp-015(隔离级别与快照语义)。

核心概念

原理与机制

InnoDB 每行隐含 trx_id(最后修改者)与 roll_pointer(指向 Undo 中的旧版本)。读时构造 ReadView:记录创建时刻的活跃(未提交)事务 ID 集合与下一个将分配的 ID。可见性规则:版本 trx_id < 低水位 → 可见;≥ 高水位 → 不可见;位于活跃集内 → 不可见——不可见就沿 roll_pointer 回溯旧版本直到可见。RC 与 RR 的差别仅在快照生成时机(每语句 vs 每事务一次)。PostgreSQL 方案更「蛮」:更新即插入新元组、旧元组标记 xmax,任何快照按 xmin/xmax 与自身事务号判断可见;旧元组成为死元组,VACUUM 负责回收——长事务会钉住旧版本,导致表与索引膨胀(bloat),这就是「PG 要盯最长事务」的原因。InnoDB 侧长事务则让 Undo 链越拉越长,purge 滞后同样拖垮全库。两条实现路线殊途同归:MVCC 把一致读的代价从「锁等待」转移为「存储膨胀 + 回溯成本」。

实例或案例

时刻线(RR 级,事务 A 先 BEGIN 后 SELECT,事务 B 随后提交):
A: BEGIN; SELECT balance;        -- 快照 v1: 1000
B: BEGIN; UPDATE balance=1200; COMMIT;
A: SELECT balance;               -- 仍读 1000:沿版本链回溯到 v1
A: UPDATE balance = balance + 1; -- 当前读!基于 1200 计算 → 提交后为 1201

最后一步是 MVCC 最反直觉的行为:同一事务内「先读后写」会从快照世界切换到最新世界,读到的值与刚写的基准不一致——这是「校验后更新」必须用 SELECT FOR UPDATE 或版本号条件更新的机制级原因。

常见误区

自测题

  1. InnoDB 的 RC 与 RR 在 ReadView 上的唯一差别是什么? 答:RC 每条语句生成新 ReadView,RR 整个事务复用第一条查询生成的 ReadView。
  2. 为什么 PostgreSQL 要求避免超长事务? 答:长事务使其开始前的版本对它可见而无法回收,死元组持续累积导致表/索引膨胀,VACUUM 与查询性能全面劣化。

公式或模型

本节不适用:可见性判定是规则集(水位比较 + 活跃集合成员判断),不是定量公式。

图示

新版本 trx=205 bal=1200 旧版本 trx=201 更旧 trx=198 roll_pointer 指向 Undo 旧版本 读事务快照若不可见 trx=205 → 回溯至 trx=201 版本
版本链:最新版本在主表,历史版本挂在 Undo,按 ReadView 回溯选取可见版本

直观类比

MVCC 像文档协作平台的「历史版本」:你按住某个时间点(快照)查看,同事随便编辑互不打扰;只有当你「基于当前最新版继续编辑」(当前读)时,才会和同事产生真正的写冲突。

与其他知识点的关系

kp-016 是实现隔离的悲观对照;kp-020 的 Undo 日志正是版本链的物理载体;kp-031 的备份一致性点依赖 MVCC 快照;kp-018 中分布式 MVCC 需要全局时间戳。

延伸阅读