PostgreSQL Field Guide

WAIT FOR LSN 与读写分离下的 read-your-writes

PostgreSQL 19 的 WAIT FOR LSN 让会话阻塞到备库追平写入 LSN,在异步备库上实现 read-your-writes,无需 synchronous_commit=remote_apply

PostgreSQL 19 仍处于 Beta

截至 2026-08,PostgreSQL 19 尚未正式发布。下文语法与行为以 PostgreSQL 19 文档为准;用于生产前请对照正式 release notes 复核。

常见的扩容做法是写走主库、读走异步备库。这个模式最脆弱的一环是写后立即读:客户端已在主库提交,但备库还没 replay 对应的 WAL,紧随其后的读会拿到旧值。这就是 stale read 问题。在 PostgreSQL 19 之前,没有服务端原语能关掉这个窗口,只能靠应用侧的各种绕法。

陈旧读问题

异步流复制下,主库的 COMMIT 不等待任何备库。WAL 记录要经传输、写入、刷盘、replay 之后在备库可见,正常是毫秒级,负载高时可能到秒级。写完立刻通过备库连接池读,就可能看不到自己刚提交的数据:

主库:  UPDATE profile SET display_name = 'new' WHERE id = 42;  -- COMMIT
备库:  SELECT display_name FROM profile WHERE id = 42;
       --> 'old'   (replay 还没追上这条 commit 记录)

这个窗口没有上界:写入突增、长事务、recovery conflict 都会让复制延迟抖动,任何“备库大概已经追上了”的固定假设迟早会失效。

PostgreSQL 19 之前的四种绕法

四种常见做法都能解决问题,但代价各不相同:

  • 写后固定 sleep。 下一次读之前睡 N 毫秒。延迟不是常量:sleep 短了,高峰期仍然读到旧值;sleep 长了,备库空闲时每个请求都白等。
  • Sticky read。 写之后的一段时间内把该会话的读钉在主库。结果正确,但把读流量推回主库的恰好是最活跃的用户——正是你卸读流量的对象。
  • synchronous_commit = remote_apply 每次 COMMIT 等待同步备库 replay 完成,改动随即处处可见。有效,但所有写入都要付备库往返延迟,且写可用性与备库健康绑定。这是集群级的持久化决策,不是按请求给的一致性提示。
  • 轮询 pg_last_wal_replay_lsn() 写后在主库记录 pg_current_wal_insert_lsn(),然后在备库上循环比较 replay 位置,直到越过目标 LSN。语义正确,但每次检查都是一次网络往返,循环占连接,超时和升主处理全靠自己写。

PostgreSQL 19 用服务端阻塞等待替代了最后一种模式。

WAIT FOR LSN 语法

WAIT FOR 让会话阻塞,直到服务器到达目标 LSN,然后返回一行状态:

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

返回值有三种:successtimeoutnot in recovery。不给 TIMEOUT(或给 0)则无限等待。超时——或者在非 recovery 状态的服务器上用 standby 模式——会报错,除非指定 NO_THROW,此时由返回的 status 列告知结果。

典型的 read-your-writes 流程:

-- 主库,写入提交后立即执行
SELECT pg_current_wal_insert_lsn();
--  0/306EE20  -> 交给应用 / 连接池保存

-- 备库,执行依赖这次写入的读之前
WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '200ms', NO_THROW);
--  status = success -> 该写入在这台备库已可见
SELECT display_name FROM profile WHERE id = 42;

取 LSN 用 pg_current_wal_insert_lsn()(insert 位置)而不是 flush 位置,这样即使写入会话使用 synchronous_commit = off 流程仍然正确。文档还指出,最后一次修改的 LSN 应保存在客户端应用或连接池一侧——WAIT FOR 本身不在语句之间记住任何状态。

四种 MODE 的语义

MODE 选择等待 WAL 处理到哪个阶段,默认是 standby_replay

MODE等待到服务器状态
standby_replayLSN 在备库 replay(应用)完成;成功后 pg_last_wal_replay_lsn() 大于等于目标值仅备库
standby_flushWAL 在备库刷盘——拿到持久化保证但不等 apply仅备库
standby_writeWAL 在备库写入操作系统——比 flush 快,持久化保证更弱仅备库
primary_flushWAL 在主库刷盘;成功后 pg_current_wal_flush_lsn() 大于等于目标值仅主库

对 read-your-writes 有意义的只有 standby_replay:replay 完成才是行对查询可见的时刻。standby_writestandby_flush 也会被备库上已有的 WAL 满足(来自 base backup 或归档恢复),不限于刚流式收到的部分。在主库上用 standby 模式、或在备库上用 primary_flush,都会报错。

限制

命令文档中的限制直接决定调用方式:

  • 必须是顶级语句。 WAIT FOR 不能在函数、存储过程或 DO 块里执行,必须由驱动或连接池中间件作为独立语句发出。
  • 不能持有快照。 命令要求当前没有 active 或 registered snapshot,因此不能用在必须保持快照的上下文——包括隔离级别高于 READ COMMITTED 的事务。实际写法:作为独立的 autocommit 语句发送,先于需要保证的那次读。
  • 升主改变答案。 等待期间备库被提升,standby 模式会返回 not in recovery(未加 NO_THROW 则报错)。升主产生新 timeline,你等待的 LSN 可能属于旧 timeline——应用必须重新评估目标是否还有意义。
  • 只比较数值。 WAIT FOR 比较的是 LSN 数值,不感知 timeline。级联备库的上游被提升后,只要 replay 位置在数值上越过目标就可能返回 success,哪怕那个位置在另一条 timeline 上。在乎这个区分就自己校验 timeline。
  • recovery conflict 仍然会打断。 备库上等待的会话可能被 recovery conflict 处理打断——有些冲突(文档举的例子是 tablespace drop)会无条件终止所有后端。调用方要准备重试和降级路径。

超时后的降级策略

WAIT FOR 当作有严格预算的一致性增强,而不是正确性原语。始终 TIMEOUTNO_THROW 成对使用,按 status 分支:

  • success——按计划在备库读。
  • timeout——降级:把这次读改发主库,或先返回旧值再异步刷新。记录超时时刻的 replay 延迟;超时率上升是备库容量问题的早期信号。
  • not in recovery——拓扑变了。重新解析主库,必要时在新主库上重取 LSN;旧 LSN 不要跨 timeline 复用。

超时值按读延迟预算定(几十到几百毫秒),而不是按平均延迟定——这个等待本来就是为尾部准备的。如果 WAIT FOR 频繁超时,说明该先治理复制延迟本身,见流复制升级与延迟控制

提示词:设计路由层

为我设计 read-your-writes 路由
为我的技术栈设计基于 PostgreSQL 19 的 read-your-writes 层。

背景:
- 语言/框架:<如 Go + pgx / Python + SQLAlchemy / Node + pg>
- 拓扑:1 主库 + <N> 个异步流复制备库,前面是 <连接池/代理>
- 典型备库延迟:p50 <ms>、p99 <ms>;写入 QPS <n>
- 写后读的延迟预算:<ms>

输出:
1. 写后在哪里取 pg_current_wal_insert_lsn()(driver hook、中间件还是连接池),以及 LSN 如何传递给后续读请求。
2. 具体的 WAIT FOR LSN 调用(MODE、TIMEOUT、NO_THROW),以及对 success / timeout / not in recovery 三种状态的伪代码分支。
3. 按端点类型(用户面读 vs 后台任务)的降级策略。
4. 指标与告警:超时率、等待时长直方图、超时时刻的 replay 延迟。
约束:WAIT FOR 必须是顶级语句,不能放在函数或 DO 块里,且不能在持有快照时使用(不能放进 REPEATABLE READ 事务)。

命令参考:PostgreSQL 19 WAIT FOR。PostgreSQL 19 其他新特性见 PostgreSQL 19 总览。实测类文章:rednafi 的完整走查digoal 的分析

Last updated on

On this page