返回
RSS ClickHouse Blog AI 逐段翻译 精选 发布 2026-08-25 08:00

PostgreSQL 19新增WAIT FOR命令实现读写一致性

DataHot 速览

PostgreSQL 19引入新的SQL命令WAIT FOR,允许会话阻塞等待WAL到达指定位置,从而在异步复制副本上实现读己之写一致性,同时避免同步复制的性能开销。该命令支持standby_replay、standby_write、standby_flush、primary_flush四种模式,并可选配置超时和异常处理。文章指出此前只能通过同步复制、应用端轮询或直接读主库来解决过期读问题。目前PostgreSQL 19仍处于beta阶段,细节可能调整。

为什么值得关注:异步复制下的读写一致性是数据平台常见痛点,WAIT FOR以SQL命令方式提供低成本解决方案,值得数据架构师和平台开发者关注。

译文

AI 逐段翻译

PostgreSQL 19 引入了一个新的 SQL 命令,WAIT FOR,它允许一个会话阻塞,直到 WAL 到达特定位置。这为我们提供了异步副本上的读己之写一致性,而无需付出同步复制的代价。

1WAIT FOR LSN 'lsn'2    [ WITH ( option [, ...] ) ];34where option can be:56    MODE 'mode'7    TIMEOUT 'timeout'8    NO_THROW910and mode can be:1112    standby_replay | standby_write | standby_flush | primary_flush

免责声明: 在我写这篇文章时,PostgreSQL 19 仍处于测试阶段,因此以下某些细节在最终版本发布前可能会有所变化。发布说明将是 19.0 正式发布后的最终依据。

过期读问题

对于异步流复制,主库上的提交不会立即在备库上可见。总是存在一定的复制延迟,通常只有几毫秒,但足以导致过期读。

让我们来看一个简单的例子:应用程序在主库上创建了一个订单,然后立即从副本读取该订单。如果副本尚未重放相关的 WAL,SELECT查询可能返回空行。实际上并没有出错,写入已提交,复制也在正常工作,只是副本尚未追赶上。我们需要一种方法告诉副本:在运行读取之前,等待直到你已看到这次写入

WAIT FOR 之前,我们有几个选项(我能想到的):

  • synchronous_commit = remote_apply:主库在每个提交时都会阻塞,直到备库已重放该提交。这保证了副本是最新的,但会给每个提交增加复制延迟,并使写入依赖于备库。
  • 应用端轮询:在写入后记录 LSN,然后在副本上反复检查 pg_last_wal_replay_lsn() 直到它追赶上。这种方法有效,但要求应用实现自己的轮询和重试逻辑。
  • 直接从主库读取:嗯,这样可行,但意味着放弃使用副本扩展读取能力的优势 😀

WAIT FOR 的作用

我将尝试解释如何使用 WAIT FOR,使用 PG19 文档 中的示例。模式很简单:写入后,从主库获取当前的 WAL 位置(使用 pg_current_wal_insert_lsn())并将该 LSN 传递给将从备库读取的会话。在那里,WAIT FOR 阻塞直到备库已将 WAL 重放到该位置,然后您再执行读取。

在主库上:

1UPDATE movie SET genre ='Dramatic'WHERE genre ='Drama';23SELECT pg_current_wal_insert_lsn();4 pg_current_wal_insert_lsn5---------------------------60/306EE20

在备库上:

1WAIT FOR LSN '0/306EE20';2 status3---------4 success56SELECT*FROM movie WHERE genre ='Drama';7 genre8-------9(0rows)

一旦 WAIT FOR 返回 成功,则保证该 LSN 之前的所有内容都已应用,读取结果反映了主库的写入。无需应用端轮询,等待期间也不持有快照;后端在闩锁上休眠,启动进程在重放到达其 LSN 时立即唤醒它。

注意: WAIT FOR 比较 LSN 位置时不理解时间线,因此在提升之后,成功可能指来自与您写入时不同的时间线的 WAL。应适当怀疑。

这里有一些见解。文档使用 pg_current_wal_insert_lsn() 函数的原因是它是最保守的选择,它涵盖了当 synchronous_commitoff 时甚至尚未刷盘的已提交事务的 WAL。

另一个优点是,与 synchronous_commit = remote_apply 不同,写入保持异步。只有需要这种保证的读取才会等待备库,而且只等到副本追赶上为止。

WAIT FOR 语法

WAIT FOR 支持不同的等待模式和几个配置选项。命令的可运行示例类似于以下内容:

1WAIT FOR LSN '0/306EE20'WITH (MODE 'standby_flush', TIMEOUT '100ms', NO_THROW);

只有 LSN 是必需的。模式选择您正在等待的 WAL 进度阶段:

模式等待直到 LSN 被运行于
standby_replay(默认)重放,因此备库上的读取可以看到更改备库
standby_flush刷新到备库磁盘(在那里持久化,但尚不可见)备库
standby_write写入备库(可能仍在操作系统缓冲区中)备库
primary_flush刷新到主库磁盘主库

在备库上,WAL 经历这些阶段:写入 → 刷新 → 重放。PostgreSQL 19 还通过 WaitForWalWriteWaitForWalFlushWaitForWalReplay 等待事件在 pg_stat_activity 中公开这些阶段,因此您可以查看会话等待的位置。(您可以在我的上一篇博客文章中阅读有关这些更改的更多信息:PostgreSQL 19 监控的新特性

TIMEOUT 限制等待时间,如果省略或设置为零,WAIT FOR 将无限期等待。默认情况下,超时会引发错误。使用 NO_THROW 时,它返回一个状态:

1WAIT FOR LSN '0/306EE20'WITH (TIMEOUT '100ms', NO_THROW);2 status3---------4 timeout

可能的状态有 successtimeoutnot in recoverynot in recovery 可能发生在主库上使用备库模式,或在等待期间备库被提升时。如果请求的 LSN 在提升前已被重放,WAIT FOR 在提升的节点上仍会返回 success

为什么它必须是顶级命令

这是我最喜欢的部分,因为它解释了为什么早期尝试此功能会遇到问题。如果您对历史感兴趣,WAIT FOR 进入 PostgreSQL 花了大约 10 年时间。 第一个提案 是在 2016 年,该功能中途被回退了三次。

WAIT FOR 不能在函数、过程或 DO 块内运行,也不能在高于 READ COMMITTED 的事务中运行。原因是快照,正如 提交信息 所解释的:

WAIT FOR 需要在没有任何快照的情况下等待。否则,快照可能会阻止 WAL 记录的重放,这暗示了一种自死锁。

持有快照的会话可以阻塞备库上的 WAL 重放。例如,重放清理记录可能需要删除旧快照仍然可见的行。Postgres 通过暂停重放来解决此冲突,并最终取消持有快照的会话。

现在想象同一个会话正在等待 WAL 重放到特定 LSN。重放被其自身的快照阻塞,而会话在重放达到其目标之前不会释放快照。这就是提交信息中提到的“自死锁”

为了避免这种情况,WAIT FOR 必须完全不持有空快照运行。SQL 函数作为查询的一部分运行,因此总是有一个快照,所以那条路径永远不会成功。相反,WAIT FOR 是一个实用命令,在顶层无快照运行,与 VACUUMCHECKPOINT 的精神一致。

Ivan Kartyshov 的 原始提案(2016) 已经认识到这个问题,并且 PostgreSQL 19 中实现的版本正是基于这一见解:

为了避免快照问题,WAITLSN 被实现为一个工具语句,这使我们能够绕过快照获取机制。

结论

我喜欢 WAIT FOR 的一点是它保持了异步复制的异步性。它不是让每次写入都等待副本,而是仅在读取确实需要读己之写保证时才付出代价。

大多数应用程序可能不会直接调用 WAIT FOR,这没关系。文档说 最后一次修改的 LSN 应该存储在客户端应用程序端或连接池端,这使得连接池和协议感知的代理成为天然的采用者。通过粘性会话,代理可以在写入后捕获 LSN,并在下一次路由到副本的读取之前注入 WAIT FOR,从而使读己之写对应用程序透明。

希望您喜欢这篇博客文章!如果您想了解更多关于 PostgreSQL 19 的信息,我将在 PostgreSQL Conference Europe 上谈论其可观测性改进,包括随该功能一起发布的新等待事件。

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存