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。理解这些差异有助于日常运维和性能优化。