副本上的 Read‑your‑writes:PostgreSQL WAIT FOR LSN 与 MongoDB Causal Consistency

发布: (2026年2月22日 GMT+8 05:36)
10 分钟阅读
原文: Dev.to

Source: Dev.to

概述

在为高可用性和可扩展性而设计的数据库中,次要节点可能会落后于主节点。通常会同步更新一组节点(法定人数)以保证持久性,同时保持可用性,而其余的备用实例则采用最终一致性,以处理部分故障。

为了在可用性和性能之间取得平衡,同步副本仅在写入已持久且可恢复时才确认写入,即使此时尚不可读取。

结果: 如果您的应用写入数据后立即查询另一个节点,仍可能看到陈旧的数据。

示例异常

您在主库提交订单后尝试从报表系统检索该订单。由于读副本尚未应用写入,订单会缺失。

PostgreSQLMongoDB 都提供避免此问题的机制,但它们的实现方式不同。

特性PostgreSQL WAIT FOR LSNMongoDB 因果一致性
时钟类型日志序列号 (LSN) – 64 位 WAL 位置混合逻辑时钟 (HLC) 时间戳
工作原理阻塞直到备用达到目标 LSN(写入、刷新或回放)在读取时附加 afterClusterTime;驱动程序跟踪 operationTime
典型用例将昂贵的读取任务卸载到副本,同时保持读写一致性会话可以从任意副本(主或从)读取
额外往返次数是 – 需要从主库获取 LSN,然后在副本上等待否 – 驱动程序自动处理时间戳
粒度LSN(每个事务)集群时间(每个操作)
可用性影响当副本追赶时可能增加延迟读取可能会短暂阻塞,直至副本追上
实现状态计划在 PostgreSQL 19 中实现(开发中)已在 MongoDB 4.0+(会话)中可用

Source:

PostgreSQL:WAIT FOR LSN(PG 19 – 正在开发中)

PostgreSQL 会在 预写日志(WAL) 中记录每一次更改。每条 WAL 记录都有一个 日志序列号(LSN)——一个 64 位的位置,通常显示为两个十六进制半段,例如 0/40002A0(高/低 32 位)。

流复制会把 WAL 记录从主库发送到备库,备库随后会:

  1. 写入 WAL 记录到磁盘
  2. 刷新 到持久存储
  3. 回放,将更改应用到数据文件
位置含义
standby_writeWAL 已写入备库磁盘(尚未刷新)
standby_flushWAL 已刷新到备库的持久存储
standby_replay(默认)WAL 已回放到数据文件并对读取者可见
primary_flushWAL 已在主库刷新(在 synchronous_commit = off 时有用)

最近一次针对 PostgreSQL 19 的提交加入了 WAIT FOR LSN 命令,允许会话阻塞,直到上述某个点达到目标 LSN。

典型工作流

  1. 在主库写入 并提交。
  2. 获取当前 WAL 插入 LSN
  3. 在副本上等待,直到它追上该 LSN。
-- 1. 在主库启动事务
BEGIN;

-- 2. 插入一行
INSERT INTO orders VALUES (123, 'widget');

-- 3. 提交事务
COMMIT;

-- 4. 获取当前 WAL 插入 LSN
SELECT pg_current_wal_insert_lsn();

结果(示例):

 pg_current_wal_insert_lsn
---------------------------
 0/18724C0
(1 row)

随后在副本上阻塞读取,直到它已回放该 LSN:

-- 在副本上
WAIT FOR LSN '0/18724C0'
  WITH (MODE 'standby_replay', TIMEOUT '2s');

如果副本在超时时间内达到请求的 LSN,命令返回;否则会因超时错误而中止。

何时使用

  • 昂贵的读取——希望将其卸载到副本,同时仍然保证读已写(read‑your‑writes)语义。
  • 事件驱动 / CQRS 架构,其中 LSN 本身充当下游消费者的变更标记。

对于许多工作负载,直接从主库读取更简单且更快。

MongoDB:因果一致性

MongoDB 使用 oplog 时间戳混合逻辑时钟(Hybrid Logical Clock,HLC) 来跟踪因果关系。

  • 在副本集里,主节点的每一次写入都会在 local.oplog.rs 中生成一条记录。
  • 每条记录都带有一个 HLC 时间戳,该时间戳将物理时间与逻辑计数器结合,形成单调递增的 集群时间
  • 副本集成员按时间戳顺序应用 oplog 条目。

由于 MongoDB 允许并发写入,“oplog 空洞” 可能出现:时间戳较晚的写入可能在时间戳较早的写入之前提交。一个天真的读取者可能会跳过较早的操作。

MongoDB 通过跟踪 oplogReadTimestamp(oplog 中最高的无空洞点)来解决此问题。副本集成员在此时间点之前的所有操作可见之前,不能读取超过该点的内容,从而在并发提交的情况下仍然保证 因果一致性

强制因果一致性

  • 驱动程序 会记录会话中最近一次操作的 operationTime
  • 当会话以 causalConsistency: true 创建时,驱动程序会在后续读取中自动加入一个等于已知最高集群时间的 afterClusterTime
  • 服务器会阻塞读取,直到其集群时间推进到 afterClusterTime 之后。

示例(Node.js 驱动)

// Start a causally consistent session
const session = client.startSession({ causalConsistency: true });

const coll = db.collection("orders");

// Write in this session
await coll.insertOne({ id: 123, product: "widget" }, { session });

// The driver automatically injects afterClusterTime into the read concern
const order = await coll.findOne({ id: 123 }, { session });

只要读取偏好允许从 secondary 以及 primary 读取,这就能保证 读写一致(read‑your‑writes) 行为。

注意: 因果一致性并不限于快照读取;它适用于所有读关注级别。会话确保后续读取至少能看到先前写入的效果,无论是哪一个副本提供了读取。

结论

PostgreSQL 和 MongoDB 都提供了在分布式环境中实现 读后写(read‑your‑writes)语义的机制,但它们的实现方式不同:

方面PostgreSQL WAIT FOR LSNMongoDB 因果一致性
底层协调器WAL 日志序列号(LSN)混合逻辑时钟(集群时间)
客户端如何参与显式获取 LSN,然后在副本上执行 WAIT FOR LSN打开因果一致会话;驱动程序处理时间戳
典型延迟影响可能会增加一次往返并在副本上等待读取可能会短暂阻塞,直至副本追上
主库 vs. 副本读取为简化起见,通常仍从主库读取读取可以安全地发送到任意副本
使用场景侧重点在保持顺序的前提下卸载大量读取在所有读取中提供会话级一致性

请选择最符合您应用的一致性需求、延迟容忍度和架构风格的方法。

WAL(LSN)中的物理字节偏移 vs. 混合逻辑时钟(HLC)

AspectPostgreSQL (WAL)MongoDB (HLC)
机制阻塞直至重放/写入/刷新 LSN 达成阻塞直至 afterClusterTime 可见
追踪应用捕获 LSN驱动追踪 operationTime
粒度WAL 记录位置Oplog 时间戳
复制模型物理流式复制逻辑 oplog 应用
空洞处理不适用(序列化 WAL)oplogReadTimestamp
故障转移处理除非使用 NO_THROW 否则报错会话继续,受复制状态限制

关键要点

  • PostgreSQL 的 WAIT FOR LSN 与 MongoDB 的因果一致性 都确保读取能够观察到先前的写入,但它们工作在不同的层次:

    • PostgreSQL 提供 手动、WAL 级别的精确度
    • MongoDB 提供 自动、会话级别的保证
  • 如果你希望“读已写”语义能够直接生效,而无需额外的协调调用,MongoDB 的 基于会话的模型 是一个强有力的选择。

  • 尽管关于一致性存在长期的误解,MongoDB 在横向可扩展的系统中提供强一致性,同时提供 简洁的开发者体验

0 浏览
Back to Blog

相关文章

阅读更多 »