编程 WAIT FOR LSN:会话级的备库 read-your-writes 与它的几个坑

2026-11-14 21:03:39

WAIT FOR LSN:会话级的备库 read-your-writes 与它的几个坑

WAIT FOR LSN 让备库等到指定 LSN 被写入、刷盘或回放之后再继续。它的典型用途是 read-your-writes:应用在主库写完,立刻去备库读自己刚写的那一行,中间用一条等待把「写」和「读」串起来。

需要先说清楚的是它的边界:这是会话级等待,不是全局一致性。它只保证发起等待的这个会话在返回后能看到该 LSN 之前的内容,不会让备库整体变成强一致读,也不会替你处理别的事务的可见性。

主库取 LSN,备库等 LSN

主库侧拿到当前写入位置,在 psql 里用 \gset 直接存成变量:

app=# INSERT INTO t (k) VALUES ('x') RETURNING pg_current_wal_flush_lsn() \gset
app=# \echo :pg_current_wal_flush_lsn
0/1A2B3C4

备库侧用同一个 LSN 发起等待,返回之后再查:

WAIT FOR LSN :'lsn';
SELECT * FROM t WHERE k = 'x';

命令形式有 WAIT 与 WAIT FOR LSN 两种,后者是显式指定 LSN 的写法。等待目标可以是写入、刷盘或回放中的某一档,按需要选择。

超时:正常返回只是一个信号

WAIT FOR LSN 正常返回时只表示条件成立,它不返回布尔值,所以调用方无法从结果里区分「等到了」和「没等到」。反过来说,如果主库没有推进 WAL 到这个 LSN,这条语句会一直挂着,不报错也不返回。

兜底手段是 statement_timeout:

SET statement_timeout = '5s';
WAIT FOR LSN :'lsn';

超时后语句报错退出,由应用决定重试还是降级去主库读。等待语句本身可以被取消,所以连接池里挂住的情况也可以通过取消或断连清理。别把 statement_timeout 设成无限,否则一旦主库停止推进,这个会话就卡死了。

隔离级别用错会直接报错

WAIT FOR LSN 对隔离级别敏感,用错会直接报错,而不是静默降级。

另一件更常见的事:等待成功了不等于读得到。等待之后需要在能看到新快照的事务里查询;如果事务的隔离级别已经把快照固定住(例如快照在读之前就已确定),那么即使 LSN 已经回放完成,该事务里依然看不到新写入的行。这类「等了却读不到」的问题,多半不是等待没生效,而是查询所在事务的快照太旧。做法是让等待后的查询落在新事务里,或者确认当前隔离级别下快照是语句级而非事务级。

锁与死锁

RC 期间修复过 WAIT FOR 相关的死锁与隔离级别报错,这两类问题在早期形态下都出现过。落到使用上有两条:

  • 不要长时间持有与复制写入路径交叉的锁。等待期间持着别的锁,会把自己和回放线程一起拖住。
  • 不要把 WAIT FOR 塞进长事务。长事务里既有快照问题,又有锁持有问题,等待时间一长,两边的代价都被放大。

比较稳妥的形态是短事务、等待放在事务内靠前的位置、等待后立刻做需要的那次读,然后提交。

用 pg_stat_recovery 排查

等待迟迟不返回时,先看 pg_stat_recovery。它给出回放位置与时间线、当前回放 WAL 记录的结束位置、当前 WAL chunk 的起始时间、promotion 状态、最近 commit/abort 的时间戳(用于判断延迟新鲜度),以及 recovery pause 状态。

排查顺序:

  1. 看最近 commit/abort 时间戳是否在动。时间戳不动,说明回放本身停了,问题不在等待语句。
  2. 比较回放位置与目标 LSN 的差距。差距在缩小就是正常追赶,等着或者放大超时;差距不动,回到第 1 步。
  3. 看 recovery pause 状态。处于 pause 说明备库被显式停住,回放不会推进,等待永远不会满足,得先恢复回放。

这三步能覆盖大部分「为什么还不返回」的情况,也能区分是主库没推进、备库被停住,还是纯粹回放速度慢。

复制全文 生成海报 PostgreSQL 复制 排障

推荐文章

程序员茄子在线接单