副本上的 Read‑your‑writes:PostgreSQL WAIT FOR LSN 与 MongoDB Causal Consistency
Source: Dev.to
概述
在为高可用性和可扩展性而设计的数据库中,次要节点可能会落后于主节点。通常会同步更新一组节点(法定人数)以保证持久性,同时保持可用性,而其余的备用实例则采用最终一致性,以处理部分故障。
为了在可用性和性能之间取得平衡,同步副本仅在写入已持久且可恢复时才确认写入,即使此时尚不可读取。
结果: 如果您的应用写入数据后立即查询另一个节点,仍可能看到陈旧的数据。
示例异常
您在主库提交订单后尝试从报表系统检索该订单。由于读副本尚未应用写入,订单会缺失。
PostgreSQL 和 MongoDB 都提供避免此问题的机制,但它们的实现方式不同。
| 特性 | PostgreSQL WAIT FOR LSN | MongoDB 因果一致性 |
|---|---|---|
| 时钟类型 | 日志序列号 (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 记录从主库发送到备库,备库随后会:
- 写入 WAL 记录到磁盘
- 刷新 到持久存储
- 回放,将更改应用到数据文件
| 位置 | 含义 |
|---|---|
| standby_write | WAL 已写入备库磁盘(尚未刷新) |
| standby_flush | WAL 已刷新到备库的持久存储 |
| standby_replay(默认) | WAL 已回放到数据文件并对读取者可见 |
| primary_flush | WAL 已在主库刷新(在 synchronous_commit = off 时有用) |
最近一次针对 PostgreSQL 19 的提交加入了 WAIT FOR LSN 命令,允许会话阻塞,直到上述某个点达到目标 LSN。
典型工作流
- 在主库写入 并提交。
- 获取当前 WAL 插入 LSN。
- 在副本上等待,直到它追上该 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 LSN | MongoDB 因果一致性 |
|---|---|---|
| 底层协调器 | WAL 日志序列号(LSN) | 混合逻辑时钟(集群时间) |
| 客户端如何参与 | 显式获取 LSN,然后在副本上执行 WAIT FOR LSN | 打开因果一致会话;驱动程序处理时间戳 |
| 典型延迟影响 | 可能会增加一次往返并在副本上等待 | 读取可能会短暂阻塞,直至副本追上 |
| 主库 vs. 副本读取 | 为简化起见,通常仍从主库读取 | 读取可以安全地发送到任意副本 |
| 使用场景侧重点 | 在保持顺序的前提下卸载大量读取 | 在所有读取中提供会话级一致性 |
请选择最符合您应用的一致性需求、延迟容忍度和架构风格的方法。
WAL(LSN)中的物理字节偏移 vs. 混合逻辑时钟(HLC)
| Aspect | PostgreSQL (WAL) | MongoDB (HLC) |
|---|---|---|
| 机制 | 阻塞直至重放/写入/刷新 LSN 达成 | 阻塞直至 afterClusterTime 可见 |
| 追踪 | 应用捕获 LSN | 驱动追踪 operationTime |
| 粒度 | WAL 记录位置 | Oplog 时间戳 |
| 复制模型 | 物理流式复制 | 逻辑 oplog 应用 |
| 空洞处理 | 不适用(序列化 WAL) | oplogReadTimestamp |
| 故障转移处理 | 除非使用 NO_THROW 否则报错 | 会话继续,受复制状态限制 |
关键要点
-
PostgreSQL 的
WAIT FOR LSN与 MongoDB 的因果一致性 都确保读取能够观察到先前的写入,但它们工作在不同的层次:- PostgreSQL 提供 手动、WAL 级别的精确度。
- MongoDB 提供 自动、会话级别的保证。
-
如果你希望“读已写”语义能够直接生效,而无需额外的协调调用,MongoDB 的 基于会话的模型 是一个强有力的选择。
-
尽管关于一致性存在长期的误解,MongoDB 在横向可扩展的系统中提供强一致性,同时提供 简洁的开发者体验。