PAYATHON 2026

PostgreSQL 18 中 pgoutput 跳过空事务后如何推进复制槽 LSN?

数据小许

结论

持续读取复制流,及时确认已经持久化的已发布事务,并在空闲期间继续发送状态反馈,让复制连接的进度反馈不再依赖收到空事务的 COMMIT。对于具备相应处理逻辑的 pgJDBC,驱动可以利用 keepalive,在没有已发布变更的区间推进 flush 位置,无须依赖 publication 之外的心跳表。

跳过空事务的优化从 PostgreSQL 15 就已存在,并非 PostgreSQL 18 首次引入。即使 PostgreSQL 17 环境此前运行正常,也需要结合实际驱动版本、反馈代码和运行配置查明原因,不能直接认定为 PostgreSQL 18 的行为回归。PostgreSQL 15 发布说明

为什么没有 BEGIN/COMMIT 仍然可以推进

对于没有需要发布的变更的普通事务,pgoutput 可以省略 BEGIN/COMMIT。这样一来,handleTxCommit() 不会被调用,txMsgsCnt == 0 的分支也不会执行,空事务本身就无法承担心跳作用。PostgreSQL 空事务优化提交

复制协议中的 keepalive 消息与 pgoutput 事务消息属于不同层次。客户端通过 standby status update 分别报告接收、flush 和 apply 位置;维持连接和报告处理进度并不要求收到业务 DML。流复制协议

先检查驱动与反馈逻辑

以 pgJDBC 42.7.5 为例,驱动内部会处理 keepalive。此前最后一条 XLogData 已被显式反馈为 flushed,且服务端位置继续前进时,驱动可以进一步更新 flush 位置。即使 readPending() 返回 null,它也可能已经处理过 keepalive。

在该版本的实现中,状态更新间隔为 0 会关闭定期反馈,但仍有 keepalive 触发的反馈路径。因此,仅凭 status-interval-sec: 0 不能断言完全没有状态更新,还需要确认自定义配置是否确实映射到了驱动参数。输入仅给出了 42.7.x,应先记录实际补丁版本,再核对对应实现。pgJDBC 42.7.5 源码

可以检查以下几处:

  • 消费线程是否持续调用 read() 或 readPending(),而不是在没有 DML 时停止读取。
  • 最后一个已发布事务处理完成后,其安全确认位置是否真正发送给了服务端。
  • 空闲刷新是否反复将应用缓存的旧 LSN 写回 setFlushedLSN(),覆盖驱动已经推进的位置。
  • 是否只有累计到 flush-messages-size: 100,或收到下一次 COMMIT,才执行反馈。

如何调整代码

业务确认与周期反馈应分开处理。下面是读取循环的结构示意,processAndAcknowledge 需要由应用实现,它不是 pgJDBC API:

long lastFeedback = System.nanoTime();
long feedbackInterval = TimeUnit.SECONDS.toNanos(1);

while (!stream.isClosed()) {
    ByteBuffer payload = stream.readPending();

    if (payload != null) {
        LogSequenceNumber messageLsn = stream.getLastReceiveLSN();
        processAndAcknowledge(payload, messageLsn);
    }

    long now = System.nanoTime();
    if (now - lastFeedback >= feedbackInterval) {
        stream.forceUpdateStatus();
        lastFeedback = now;
    }

    if (payload == null) {
        TimeUnit.MILLISECONDS.sleep(10);
    }
}

示例中的 1 秒和 10 毫秒只是调度示例,并非官方推荐值。将所有流操作放在同一线程中执行,便于保持读取、处理和反馈的顺序。

processAndAcknowledge 必须先完成协议解析和事务处理。只有确认完整事务已达到应用要求的持久化边界后,才能执行类似操作:

// durableCommitLsn 由应用根据已完成事务的安全边界确定。
stream.setFlushedLSN(durableCommitLsn);

// 仅在该事务也已经完成应用时设置。
stream.setAppliedLSN(durableCommitLsn);

stream.forceUpdateStatus();

空闲分支只发送当前状态,避免无条件写入缓存的旧位置。forceUpdateStatus() 本身也不会把尚未处理的事务变成“已完成”。

不要在每次 readPending() 返回 null 时,直接将 getLastReceiveLSN() 同时设为 flushed 和 applied。没有新 payload,不代表此前收到的事务已经提交,也不代表异步处理队列已经持久化完成。

如何确认修复有效

先查询复制槽状态:

SELECT slot_name,
       active,
       restart_lsn,
       confirmed_flush_lsn,
       wal_status
FROM pg_replication_slots
WHERE slot_name = 'repslot_test';

在测试环境中,先提交一个涉及 publication 内表的事务,等待消费端完成持久化并发送确认,然后只修改 publication 外的表。观察消费者持续读取、发送状态反馈后,confirmed_flush_lsn 是否继续前进。

这个测试可以检查最后一条业务消息是否始终没有被显式确认,导致后续空闲进度无法按预期推进。同时记录接收位置、应用确认位置、驱动 flush 位置和每次反馈时间,定位停滞发生在哪一环。

confirmed_flush_lsn 与 restart_lsn 的含义不同。前者表示消费者确认的位置,后者表示复制槽仍可能需要的最早 WAL 位置;确认位置前进,不意味着保留的 WAL 会立即全部释放。复制槽状态说明

需要显式心跳时怎么办

如果应用仍需要可见的业务层进度标记,可以将专用心跳表加入 publication,并在消费端识别它。这样会产生实际的发布事件,也需要相应的处理逻辑。

proto_version=4 不提供恢复空事务 BEGIN/COMMIT 的开关。如果选择逻辑消息作为标记,需要启用 messages 并实现对应的消息解析;它也不能绕过此前业务事务的持久化确认。pgoutput 协议参数

不要直接把 pg_current_wal_lsn() 的查询结果作为已消费位置进行确认。它表示服务端的 WAL 位置,无法证明消费端已安全处理此前所有已发布变更。

备注:内容仅供参考。