一、结论先行
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 调优