由于使用了回写缓存,PostgreSQL 在增删改数据时并未真的写入磁盘。而缓存通常在内存中,断电后会丢失数据,进而破坏数据库的持久性(Durability)。 因此需要使用预写式日志(Write-Ahead Log)保证在系统重启后能够恢复断电前的数据。

数据库使用回写策略的原因是在实际的工作负载中存在大量的随机写入,而磁盘的随机写入速度非常缓慢,因此透写策略的性能会非常糟糕。另一方面回写策略会将修改后的数据保留在缓存中一段时间,这段时间内新的修改同样只会作用在缓存上,并在被替换时将所有的修改最终合并为一次写入。

数据库的持久性要求数据库在向用户回报事务完成之前必须将数据写入到持久化的存储介质中。 如果在缓存被替换完成写入后才向用户回报事务已完成,往往会造成下游业务不可接受的等待时间。 如果要求缓存在较短时间必须写入一次,又会在一定程度上退化成透写策略造成写入性能下降。

此外,磁盘的写入操作通常不是原子的。如果将数据直接写入原先的位置替换原本的数据但在写入中途意外断电,则很有可能导致磁盘的数据出现不一致。例如前半部份是新的数据,而后半部份是原先的数据。重启恢复时既无法完成写入操作,也无法回滚至原先的状态。

上述两个问题都可以通过将对随机页面的写入抽象为一系列连续的操作日志再写入磁盘解决。 在重启恢复时,只需要按照日志重新执行一遍修改操作即可恢复缓存的状态,进而保证数据库的持久性。

日志同样可以被缓存,在经过一小段时间或者日志数量较多时才会被写入磁盘。考虑到磁盘的顺序写入速度通常大于随机写入速度,因此写入连续日志的性能相当可观,并不会出现随机访问性能退化的问题。 这种方式也规避了写入中途断电带来的不一致性。数据库只有在日志写入完成后才会向用户回报操作已完成。若在恢复阶段读到了写入到一半的日志,直接结束恢复阶段即可,此时的状态即为停机前回滚了未完成事务时的状态。

在崩溃前,一部分数据页面可能已经被写入到磁盘中,而另一部分数据页面仍然在缓存中并在崩溃后丢失。 因此在恢复阶段重放日志时可能会将部分较新的数据重新修改回旧值甚至不一致的状态,但是因为日志操作的幂等性,在完整重放完成后数据一定能够恢复到崩溃前一致的状态。

然而,从数据库创建到崩溃的日志可能会非常庞大。而大部分比较久远的日志所包含的操作已经被写入到数据页面中,重放这部分日志只会拖慢恢复速度。 因此 PostgreSQL 需要定期清理过时的日志。

理论上数据库可以安全地删除 的日志,其中 是所有已经写入磁盘的数据页面的对应的最小日志序列号(Log Sequence Number,LSN)。 也等于缓存中所有脏页面在磁盘中的 LSN。

在实现中 PostgreSQL 则会主动写入部分脏页面到磁盘,以尽可能地减少保留的日志数量。 假设 PostgreSQL 希望清理编号 的日志,仅保留编号大于 的日志,PostgreSQL 会扫描并记录日志 中所有操作的页面,并将缓存中位于这个页面列表中的脏页面写入磁盘。 直接写入缓存脏页面也不是尝试重放日志是因为缓存中已经有了这些页面的最新版本。即使缓存中的页面版本比日志 记录的版本更新,由于日志的幂等性,在更新的数据页面中重放 的日志也不会对数据造成影响。 写入完成后即可安全地删除 的日志,并且下次恢复时只需要重放编号大于 的日志即可。