TL;DR

MySQL InnoDB 和 PostgreSQL 都实现了 MVCC,但底层机制截然不同:

  • InnoDB:undo log + 回滚段,历史版本链在外部
  • PostgreSQL:堆内多版本,tuple 版本由 xmin/xmax 系统列管理

InnoDB:Undo Log 方案

InnoDB 的每一行数据在聚簇索引中只存当前版本。历史版本存在 undo log 中,通过 DB_ROLL_PTR 形成版本链:

聚簇索引记录(当前版本)
┌─────────────────────────────┐
│ id=1, amount=100            │ ← 当前版本
│ DB_ROLL_PTR ────────┐       │
└──────────────────────┼──────┘


      Undo Log(历史版本链)
      ┌─────────────────────┐
      │ amount=80  (tx=102) │
      │ DB_ROLL_PTR ──┐     │
      └────────────────┼────┘

      ┌─────────────────────┐
      │ amount=50  (tx=99)  │
      │ DB_ROLL_PTR = NULL  │
      └─────────────────────┘

关键特点

  • 回滚段 (Rollback Segment) 存放 undo log,purge 线程定期清理
  • ReadView 在第一次读取时创建,保存当前活跃事务列表
  • 版本链从当前 → 历史回溯寻找可见版本

PostgreSQL:堆内多版本

PostgreSQL 在同一堆页面中保留多个 tuple 版本,通过 xmin / xmax 系统列标记可见性:

┌─────────────── Heap Page ───────────────────────┐
│  Tuple 1:  xmin=103, xmax=0     (当前版本)       │
│  Tuple 2:  xmin=100, xmax=103  (旧版本,已更新)   │
│  Tuple 3:  xmin=98,  xmax=100  (更旧版本)        │
└─────────────────────────────────────────────────┘

关键特点

  • 更新 = 标记旧 tuple xmax + 插入新 tuple(insert-only)
  • 没有 undo log,旧版本直接留在数据页中
  • VACUUM 负责回收 dead tuple 空间
  • 快照 (Snapshot) 基于 xmin/xmax/xip 三要素判断可见性

核心差异对比

维度 InnoDB PostgreSQL
历史版本存储 Undo log(外部) 堆内(同页)
更新开销 需要写 undo + 修改原行 仅插入新 tuple
回表代价 需沿版本链回溯 直接在页内
空间回收 Purge 线程 VACUUM
回滚效率 直接应用 undo 标记不可见即可
表膨胀 较轻 需要定期 VACUUM

什么场景选哪个?

  • 高更新频率(OLTP)→ InnoDB 更稳定,undo log 独立回收
  • 分析型查询 → PostgreSQL 的堆内多版本可能更优(减少 IO)
  • 长事务 → PostgreSQL 需要小心 table bloat,InnoDB 的 undo log 受事务时间影响

总结

两种实现没有绝对的优劣。InnoDB 的 undo log 方案在更新密集型场景更健壮,PostgreSQL 的堆内多版本更简洁直观但依赖 VACUUM。理解这些差异有助于日常运维和性能优化。

参考