PostgreSQL 19 引入了新的 SQL 命令 WAIT FOR。它允许会话阻塞,直到 WAL 到达指定的 LSN 位置。这个能力解决了异步复制架构中的一个常见问题:写请求已经在主库成功提交,但紧接着的读请求被路由到副本时,副本可能还没有应用这次写入。
过去,想要稳定获得“读己之写”一致性,常见方案是启用同步复制,或者在应用层维护复杂的重试和延迟逻辑。WAIT FOR 提供了另一条路径:继续使用异步副本,同时让需要一致性保证的请求等待副本追上指定的 WAL 位置。
为什么异步副本会读不到刚写入的数据
一次写事务提交后,主库会产生对应的 WAL。异步复制下,WAL 从主库传输到副本,再由副本重放,中间存在时间差:
客户端写入主库
|
v
主库提交事务,生成 WAL
|
+--> 客户端立即读取副本
|
v
副本接收并重放 WAL
如果读请求到达副本的时间早于 WAL 重放时间,应用可能观察到旧数据。这个问题在“创建订单后立即查询订单状态”“修改配置后立刻读取配置”等流程中尤其明显。
同步复制可以缩小这个窗口,但会让主库提交等待副本确认,从而增加写延迟,并降低系统对副本网络和故障的容忍度。WAIT FOR 的思路是把等待放在真正需要一致性的位置,而不是让所有写事务都承担同步复制成本。
用 LSN 把写入和读取关联起来
LSN,也就是 Log Sequence Number,表示 WAL 中的位置。应用可以在主库写入完成后取得一个 LSN,并把它传递给后续的副本读取流程。
下面是一个可以按 PostgreSQL 19 环境改造的最小示例。假设应用使用同一个数据库用户连接主库和只读副本:
-- 在主库执行:完成写入后记录当前 WAL 位置
BEGIN;
INSERT INTO orders (customer_id, status)
VALUES (42, 'created');
COMMIT;
SELECT pg_current_wal_lsn() AS write_lsn;
应用保存查询结果中的 write_lsn,例如 0/16B6C50,然后在副本连接上执行:
-- 在异步副本执行:等待副本追上主库写入位置
WAIT FOR LSN '0/16B6C50';
SELECT id, customer_id, status
FROM orders
WHERE customer_id = 42
ORDER BY id DESC
LIMIT 1;
这里的 LSN 必须来自本次写入之后的 WAL 位置,并由应用传递给副本连接。实际项目中,通常会把它放入请求上下文、会话令牌或内部 RPC 参数,而不是让客户端自行构造。
WAIT FOR 的完整形式还支持等待模式、超时以及 NO_THROW 选项:
WAIT FOR LSN '0/16B6C50'
WITH (
MODE 'mode',
TIMEOUT 'timeout',
NO_THROW
);
摘要没有规定具体 mode 和 timeout 的取值,因此接入 PostgreSQL 19 时应以目标版本的官方语法和部署策略为准。NO_THROW 适合希望把“尚未追上”作为普通业务结果处理的场景;没有这个选项时,超时或等待失败应按数据库错误处理。
把等待放进请求流程
可以这样设计一个读写分离请求:
1. 将写请求发送到主库。
2. 提交事务。
3. 在主库取得 write_lsn。
4. 把 write_lsn 传递给后续读取请求。
5. 读取请求连接到副本。
6. 副本执行 WAIT FOR LSN 'write_lsn'。
7. 等待成功后再执行查询。
伪代码示例:
# 假设 db_primary 和 db_replica 是已建立的数据库连接。
with db_primary.cursor() as cur:
cur.execute(
"INSERT INTO orders (customer_id, status) VALUES (%s, %s) RETURNING id",
(42, "created"),
)
order_id = cur.fetchone()[0]
db_primary.commit()
cur.execute("SELECT pg_current_wal_lsn()")
write_lsn = cur.fetchone()[0]
with db_replica.cursor() as cur:
# write_lsn 来自受信任的主库连接,不应直接拼接用户输入。
cur.execute("WAIT FOR LSN %s", (write_lsn,))
cur.execute("SELECT id, customer_id, status FROM orders WHERE id = %s", (order_id,))
order = cur.fetchone()
上面的 Python 片段展示的是调用结构,具体驱动是否支持把 LSN 作为参数传给 WAIT FOR,需要以所用驱动和 PostgreSQL 19 的参数化语法为准。若驱动不支持该语句的参数化,可以使用严格校验后的 LSN 字符串生成 SQL;不要把未校验的请求参数直接拼进 SQL。
这不是所有请求都需要的等待
WAIT FOR 的价值在于精确控制一致性成本,而不是把每次读取都变成同步读取。可以根据业务路径区分:
- 用户刚完成写入,随后必须看到结果的请求:等待对应 LSN。
- 搜索、报表、推荐等允许短暂延迟的读取:直接访问副本。
- 对延迟极其敏感的写入:继续评估是否需要同步复制或主库读取。
- 副本延迟持续过高的情况:设置超时,转回主库或返回可重试结果。
需要注意,等待本身并不会修复副本故障、复制中断或长期积压。它只是让当前会话等待目标 WAL 位置;如果副本无法追上,超时策略和故障转移策略仍然必须由应用负责。
接入清单
采用 PostgreSQL 19 的 WAIT FOR 前,可以检查以下事项:
- 主库写事务提交后能取得并传递对应的 WAL 位置。
- 读请求知道自己连接的是哪个副本。
- 等待存在明确的超时边界。
- 超时后的行为已经定义,例如转读主库、重试或返回状态码。
- LSN 只来自受信任的数据库结果,避免 SQL 注入。
- 监控副本 WAL 接收、重放延迟和
WAIT FOR超时次数。
对异步复制系统而言,这个命令把“是否已经看到这次写入”的判断从应用层轮询,变成了数据库能够理解的 WAL 位置等待。它不会消除复制延迟,但可以让一致性要求集中在少数真正需要它的请求上。