PostgreSQL vs MySQL 日志机制深度对比

一、结论先行

PostgreSQL 没有 redo log、binlog、undo log。

只有 WAL 一套日志,承担了 MySQL 三套日志的全部职责。


二、MySQL 为什么需要三套日志

MySQL 的架构分为两层,导致日志也分裂成两层:

复制代码
MySQL 架构
┌─────────────────────────────────────┐
│           Server 层                  │
│   binlog(引擎无关,Server 自己记)   │
├─────────────────────────────────────┤
│         InnoDB 引擎层                │
│   redo log(InnoDB 自己记)          │
│   undo log(InnoDB 自己记)          │
└─────────────────────────────────────┘
  • binlog:Server 层的逻辑日志,记录"执行了什么操作"
  • redo log:InnoDB 的物理日志,记录"数据页哪里改了"
  • undo log:InnoDB 的回滚日志,记录"改之前是什么"

这是历史包袱:MySQL 最初是插件式引擎架构,Server 层不知道引擎内部,只能各记各的。


三、三套日志各自的职责

3.1 redo log ------ 崩溃恢复

复制代码
写入时序:
  事务提交
    ↓
  写 redo log(WAL,顺序写,快)
    ↓
  内存数据页标记为 dirty
    ↓
  checkpoint 时才把 dirty page 刷盘

崩溃恢复:
  重启 → 读 redo log → replay 未刷盘的变更 → 数据一致

对应 PostgreSQL:WAL 承担了完全相同的职责


3.2 binlog ------ 主从复制 / 数据归档 / CDC

复制代码
binlog 有三种格式:
  STATEMENT:记录 SQL 语句(不精确,有歧义)
  ROW:      记录行的前后变化(精确,CDC 必用)
  MIXED:    混合模式

主从复制流程:
  主库 binlog → IO Thread → 从库 relay log → SQL Thread → 从库执行

对应 PostgreSQL:WAL logical decoding 承担了相同职责


3.3 undo log ------ 事务回滚 + MVCC

复制代码
UPDATE user SET name='Alice' WHERE id=1

InnoDB 做法:
  1. 把旧值 name='Bob' 写入 undo log
  2. 数据页改为 name='Alice'(原地更新)
  3. 事务回滚时:从 undo log 读旧值,恢复回去
  
MVCC 读旧版本:
  另一个事务需要读 name='Bob'(旧快照)
  → 从 undo log 回溯旧版本

对应 PostgreSQL:不需要 undo log,见下节


四、PostgreSQL 为什么不需要 undo log

这是 PG 和 MySQL MVCC 实现思路的根本差异

MySQL:原地更新(Update in Place)

复制代码
数据页:
  ┌──────────────────┐
  │  id=1, name=Alice │  ← 只存最新版本
  └──────────────────┘

undo log:
  ┌──────────────────┐
  │  id=1, name=Bob   │  ← 旧版本存这里
  └──────────────────┘

读旧快照 → 去 undo log 里找

PostgreSQL:追加写(Append-Only Heap)

复制代码
数据页:
  ┌──────────────────────────────────────────────┐
  │  Tuple1: id=1, name=Bob  [xmax=200, dead]    │  ← 旧版本,打标记
  │  Tuple2: id=1, name=Alice [xmin=200, alive]  │  ← 新版本
  └──────────────────────────────────────────────┘

UPDATE = INSERT 一条新 tuple + 把旧 tuple 标记为 dead

读旧快照 → 直接读同一数据页里的 dead tuple(根据 xmin/xmax 判断可见性)

代价:需要 VACUUM 清理

复制代码
dead tuple 不会自动消失,需要 VACUUM 定期清理:
  VACUUM table_name;
  -- 或者 autovacuum 后台自动运行

如果 VACUUM 跑不过来(写入太快)→ 表膨胀(Table Bloat)
这是 PG 的独有问题,MySQL 没有

五、MySQL 的两阶段提交

由于 redo log 和 binlog 是两套独立的日志,MySQL 必须用两阶段提交保证一致性:

复制代码
事务提交流程:
  1. InnoDB 写 redo log(prepare 阶段)
  2. Server 写 binlog
  3. InnoDB 提交 redo log(commit 阶段)

如果第 2 步崩溃:
  重启后发现 redo log prepare 但 binlog 没有 → 回滚
如果第 3 步崩溃:
  重启后发现 binlog 有但 redo log 未 commit → 提交

保证 redo log 和 binlog 始终一致

PostgreSQL 没有这个问题,因为只有一套 WAL,天然一致。


六、完整对比表

职责 MySQL PostgreSQL
崩溃恢复 redo log(InnoDB 层) WAL
主从复制 binlog(Server 层) WAL Streaming Replication
CDC / 变更捕获 binlog(ROW 格式) WAL Logical Decoding
事务回滚 undo log WAL(未提交事务直接丢弃)
MVCC 旧版本 undo log 数据页内的 dead tuple
日志套数 3 套 1 套
架构复杂度 高(两阶段提交协调)

七、各自的优缺点

PostgreSQL WAL 优点

复制代码
✅ 架构简洁,一套日志全包
✅ 无两阶段提交开销
✅ WAL 顺序写性能极好
✅ Logical Decoding 开箱即用,CDC 友好

PostgreSQL WAL 缺点 / 注意事项

复制代码
⚠️ dead tuple 堆积需要 VACUUM,写入密集场景需要调优
⚠️ wal_level=logical 会增加 WAL 体积(记录更多信息)
⚠️ Replication Slot 如果消费方停止,WAL 文件不删除 → 磁盘风险

MySQL 日志优点

复制代码
✅ undo log 独立,旧版本存储与数据文件分离,表不膨胀
✅ binlog 格式成熟,生态丰富(Canal、Maxwell 等 CDC 工具)

MySQL 日志缺点

复制代码
⚠️ 三套日志,维护复杂
⚠️ 两阶段提交有额外开销
⚠️ binlog 和 redo log 可能不一致(极端崩溃场景)
⚠️ undo log 过大会影响读性能(长事务问题)

八、小结

复制代码
MySQL:历史包袱导致三套日志
  redo log  ← 崩溃恢复
  binlog    ← 复制/CDC
  undo log  ← 回滚/MVCC

PostgreSQL:设计简洁,一套 WAL 全包
  WAL              ← 崩溃恢复 + 复制 + CDC
  heap dead tuple  ← MVCC 旧版本(替代 undo log)
  
代价不同:
  MySQL  → 两阶段提交协调开销 + undo log 长事务问题
  PG     → dead tuple 堆积需要 VACUUM 调优