提到事务的回滚能力,熟悉Oracle的开发者第一反应往往是回滚段(rollback segment),而转到PostgreSQL后,会发现体系结构里根本没有这个组件,却照样能回滚事务、保证ACID特性。PostgreSQL的做法是把宝押在两套机制上:一套是WAL预写日志,负责持久性和崩溃恢复;另一套是MVCC多版本并发控制,负责并发隔离和回滚语义。理解这两套机制如何配合,是掌握PostgreSQL内核行为、排查日志膨胀、优化性能绕不开的一课。

WAL预写日志:先写日志再改数据
WAL的全称是Write-Ahead Logging,核心规则只有一条:对数据文件的任何修改,必须先把这次修改对应的日志记录刷到磁盘,然后才能把脏数据页写回磁盘。这个顺序看似简单,却是PostgreSQL保证持久性的基石。因为日志记录是顺序追加的,而数据页是随机读写的,先顺序写日志、延迟刷数据页,既保证了崩溃后能恢复,又大幅减少了随机IO。
每条WAL记录由XLogRecord结构构成,包含事务ID、页面修改前后的镜像(取决于是否开启full_page_writes)、以及备份数据块等。事务提交时,PostgreSQL会写入一条提交记录并执行XLogFlush把日志刷盘,刷盘成功的那一刻,事务才算真正提交完成。如果之后进程崩溃,重启时从最近的checkpoint点开始重放WAL,已提交的事务全部恢复,未提交的事务因为找不到提交记录而作废,这就是所谓的redo阶段。
WAL默认存放在数据目录的pg_wal子目录下,以16MB一个的段文件组织。控制其行为的关键参数包括wal_level(决定日志信息量,流复制至少要设为replica)、synchronous_commit(控制提交时是否同步刷盘)、max_wal_size(限制WAL总量,影响checkpoint频率)。查看当前WAL状态可以用下面这类SQL:
-- 查看当前WAL的LSN位置 SELECT pg_current_wal_lsn(); -- 查看WAL段文件列表 SELECT * FROM pg_ls_waldir() ORDER BY modification DESC LIMIT 5;
MVCC与clog:PostgreSQL的回滚为什么不需要撤销数据
Oracle的回滚段存的是修改前的旧数据,回滚时把旧值写回去。PostgreSQL反其道而行:更新一行时,旧版本元组并不删除,而是插入一个新版本元组,新旧版本通过
那事务状态存哪里?答案是clog,即事务提交日志(commit log)。clog是共享内存中一小块位图在磁盘上的持久化形式,每个事务只用2个bit表示四种状态:进行中、已提交、已中止、已提交子事务。回滚在PostgreSQL里是极其轻量的操作:只需要把clog中该事务的状态标记为aborted,然后清理一些内存结构,完全不用去物理恢复任何数据页。后续任何事务读到这个旧版本元组时,一查clog发现xmax事务已中止,就知道这行其实没被删掉,直接无视即可。
-- 查看一行的多版本链 SELECT ctid, xmin, xmax, * FROM your_table WHERE id = 1; -- 查看表和库的膨胀情况 SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
这种设计的代价是死元组会留在表里,必须依赖vacuum周期性清理,否则表和索引持续膨胀,也就是常说的bloat问题。这也是PostgreSQL与undo方案最典型的差异:Oracle把清理成本放在回滚时或回滚段管理上,PostgreSQL把成本推迟给了后台的autovacuum。
checkpoint与崩溃恢复流程
如果每次脏页都立即写回磁盘,WAL就失去意义了。PostgreSQL用checkpoint机制来控制脏页刷盘节奏:checkpoint发生时,系统把当前所有脏页刷到磁盘,并在WAL中写入一条checkpoint记录,同时更新pg_control文件中的重放起点。之后一旦崩溃,恢复只需从最近的checkpoint开始重放WAL,而不是从头重放全部历史。
checkpoint的触发由max_wal_size和checkpoint_timeout共同决定:WAL写入量达到max_wal_size或距上次checkpoint超过timeout时长,就会触发。刷脏的工作被分散在checkpoint进程的多个周期内平滑完成,避免瞬时IO尖峰。相关参数checkpoint_completion_target建议设为0.9,让刷页尽量铺满整个间隔。通过下面的视图可以观察checkpoint是否过于频繁:
-- 需要超级用户,查看checkpoint统计
SELECT checkpoints_timed, checkpoints_req,
checkpoint_write_time, buffers_checkpoint
FROM pg_stat_bgwriter;
如果checkpoints_req(请求触发的checkpoint)占比很高,说明max_wal_size偏小,WAL切换太频繁,写放大严重,适当调大该值通常能明显平滑IO。
WAL与undo日志的设计权衡
把PostgreSQL和MySQL InnoDB对比更能看清设计取舍。InnoDB既有redo log又有undo log,回滚时用undo把数据物理还原,长事务回滚可能非常耗时。PostgreSQL没有undo,回滚瞬间完成,代价是表膨胀风险和vacuum的持续开销;另外长事务会阻碍vacuum清理死元组,进一步放大膨胀,这也是生产环境必须监控长事务的原因。
对于常见问题,几条实践建议:一是控制长事务,用idle_in_transaction_session_timeout清理挂起的空闲事务;二是合理配置autovacuum的autovacuum_vacuum_scale_factor,大表建议单独调低;三是在大量更新后手动执行VACUUM ANALYZE;四是监控pg_wal目录大小,异常增长往往意味着归档滞后或复制槽未消费,可用pg_replication_slots排查。理解了WAL负责持久、MVCC负责可见性这一分工,PostgreSQL的事务行为就不再神秘,遇到日志膨胀和表膨胀问题也能快速定位根源。
PostgreSQL事务日志WALMVCC修改时间:2026-09-15 22:14:48