在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

多版本并发控制(MVCC)

MVCC 为同一行保留多个逻辑版本,让读写能够交错执行。读取不会直接拿最新存储值,而是取得快照,从新到旧检查版本,返回第一个创建者与替代者都符合该快照可见性规则的版本。

如果 PostgreSQL 的 autovacuum 在运行,但频繁更新的表仍不断积累失效行版本,可以检查长事务和旧快照。本课展示这种现象的机制:稳定读取需要历史,回收边界被固定时,历史版本会继续积累。

默认行是一笔余额为 $100 的账户。T1 取得快照 S1,T2 随后创建 $120 版本并提交。T1 仍读到 $100,后来的新快照读到 $120。两次读取对应不同的已提交视图,结果都正确。

MVCC 如何工作

本课采用带有 PostgreSQL 特征的简化可见性规则。每个逻辑行版本记录创建事务、创建者的进行中/已提交/已中止状态、替代该版本的事务,以及替代者是否在读者快照中可见。

版本要通过两道检查:创建者在快照 S 中已提交;替代者不存在,或未在 S 中提交。按版本从新到旧检查,返回第一个通过者。

V0 保存 $100,由已提交的 T0 创建。T1 的首个 SELECT 取得 S1,已提交集合为 {T0}。V0 的创建者在集合中,也没有 S1 可见的替代者,因此通过。

T2 更新时不会覆盖 V0。先把 T2 记为 V0 的替代者,再追加由 T2 创建的 V1=$120。T2 仍在进行中,V1 的创建者检查不通过;T2 不在 S1 的已提交集合中,V0 仍通过。模型中的 T1 无须等写者,便可返回 $100。

T2 提交改变事务状态,不改变 S1。对 T1,V1 仍因 T2 不在 S1 中而失败,V0 仍因替代者不在 S1 中而通过。后来的 S2 包含 {T0,T2},V1 才成为该读者的最新可见版本。

存储状态与可见状态不同。V0、V1 可以同时保存:V1 对新快照生效,V0 对旧活动快照仍是正确答案。

逐步演示

  1. 初始 V0=$100,创建者 T0 已提交。
  2. T1 开始。在 Repeatable Read 下,BEGIN 本身不取得此处的事务快照;首个语句确定 S1={T0}。
  3. T1 检查 V0,两道检查都通过,返回 $100。
  4. T2 开始 UPDATE balance = 120,V0 保留,元数据记录进行中的替代者。
  5. T2 追加 V1=$120。T1 先检查 V1,因 T2 未在 S1 中提交而拒绝,再选择 V0,第二次返回 $100。
  6. T2 提交。未来快照可包含 T2,S1 仍为 {T0}。
  7. T1 再查。V1 已全局提交,但仍不属于 S1,故第三次返回 V0 的 $100。
  8. 后来的 T3 取得 S2={T0,T2},V1 两道检查通过,返回 $120。
  9. 清理检查 V0,但 S1 早于 T2 的替代提交,仍可见 V0,不能回收。
  10. T1 结束,释放 S1。清理移除 V0,只剩 V1。

最终读取账本为:T1 的 $100 → $100 → $100,T3 的 $120。

把隔离级别改成 Read Committed,保留相同更新顺序。T1 首个语句用 S1 返回 $100;T2 进行中时的新语句取得 S2,仍排除 T2,返回 $100;T2 提交后的语句取得 S3={T0,T2},返回 $120。变化来自快照寿命,不是更新方式。此例中,Repeatable Read 保持事务内视图稳定,Read Committed 为每条语句取得更新的已提交视图。

为什么旧版本会积累

提交新值不代表所有旧版本都可删除。可见性检查问“一个快照选择哪个版本”;清理检查问“全局回收边界是否已推进,足以证明旧版本不再需要”。

Long snapshot 预设一直保留 T1 的 S1,T2、T3、T4 依次提交 $120、$140、$160。新快照会看到这些新值,S1 却排除三个写者。T1 拒绝 V3、V2、V1 后,仍选 V0=$100。

计数从 stored 1 / obsolete 0 变为 stored 4 / obsolete 3。T2 后边界阻止回收 {V0},T3 后为 {V0,V1},T4 后为 {V0,V1,V2}。

S1 直接选择的只有 V0,并不选择 V1、V2。但这个 PostgreSQL 风格的保守回收模型中,S1 将边界固定在替代事务之前,因此三个失效版本都保留。T1 结束后边界推进,V0、V1、V2 分步回收,留下 V3=$160。

历史让读者保持旧视图,持续更新也会增加存储与清理工作。“已提交”“对 S1 可见”“可安全回收”是三种判断。

若一行保存 V 个版本,本教学模型最多检查 V 个候选;固定预设最多四个版本。真实引擎的元数据、索引、页布局、清理方式各不相同,这不是所有数据库通用的运行时间公式。

边界情况

  • 写者仍在进行中:V1 的创建者检查失败,读取选择快照允许的旧版本。
  • 写者在快照之后提交:全局已提交仍可能不属于旧快照,Repeatable Read 保留这一区别。
  • 新语句快照:Read Committed 可包含前一条语句尚未包含的提交,同一事务两条语句可返回不同已提交值。
  • 更新中止:Aborted update 预设把 V1 创建者标为中止。旧快照和新快照都拒绝 V1,选择 V0。清理可移除中止版本,不会删除它未能替换的已提交值。
  • 多个失效版本:旧快照直接选择最老版本,固定的回收边界仍可能同时保留中间历史。
  • 不再有活动快照固定边界:事务快照结束或语句快照释放后,清理目标可能变得可回收。

常见错误

  • 把最新存储版本当成所有读者都可见的版本。
  • 认为所有隔离级别都在 BEGIN 取得快照。本课的 PostgreSQL Repeatable Read 流程在首个语句取得事务快照。
  • 认为提交会修改已存在的快照。它改变未来快照可包含的事务。
  • 认为 MVCC 消除全部等待。本课只隔离展示一次更新中的非阻塞读取,省略写写冲突等等待。
  • V1 一提交就删除 V0,破坏 S1 重复读取原值的能力。
  • 把可见性与清理当成同一测试。S1 不选择的中间版本也可能被保守边界保留。
  • 混同行版本、事务与快照。T2 创建版本,S1 描述已提交视图。
  • 把 created by、superseded by 当成 PostgreSQL 元组的实际列名。它们是逻辑标签。

模型范围

动画用版本卡片与显式已提交事务集合,不复制物理 PostgreSQL 元组链、undo 日志引擎或全部 MVCC 规则。省略 WAL、缓冲池、索引、写写冲突锁、事务自己的写入、子事务、Serializable Snapshot Isolation、HOT 链、事务 ID 回卷及真实 vacuum 调度。

不同引擎的布局、可见性元数据、隔离保证和回收机制不同。模型把两者分开:快照通过两道检查选择版本,保守的 PostgreSQL 风格边界决定失效版本何时可回收,不代表 PostgreSQL 的精确 vacuum 实现。