2. 变更流底层原理与oplog机制
Change Streams 并不是 MongoDB 在数据集合之上单独维护的一套“事件总线”,它本质上是对副本集 oplog 的一种受控、可恢复、可过滤的读取封装。理解这一点,是掌握变更流行为边界的关键:为什么变更流只能在副本集或分片集群上使用、为什么它天然具备“至少一次”语义、为什么恢复令牌能实现断点续传,答案都藏在 oplog 的写入与读取机制里。
oplog 是什么:副本集的“操作日志环”
在副本集中,主节点(Primary)处理写请求时,会把每一次数据变更记录成一条幂等的操作条目,写入本地 local.oplog.rs 集合。从节点(Secondary)持续拉取这些条目并在本地重放,从而保持与主节点的数据一致。oplog 具有三个决定性特征:
- 有序性:每条 oplog 条目都带有一个单调递增的时间戳
ts,形如Timestamp(1700000000, 1),由秒级时间与序号两部分组成,保证全局顺序。 - 幂等性:条目记录的是“把某文档改成什么样”,而不是“加一”这类相对操作,因此重复重放不会破坏一致性。
- 有界性:oplog 是一个固定大小的环形集合(capped collection),写满后会覆盖最旧的条目。这意味着变更流能回溯的历史深度,受限于 oplog 窗口。
变更流读取的正是这些条目,但它并不直接暴露原始 oplog,而是经过一层“事件化”转换:把 i(insert)、u(update)、d(delete)等操作,翻译成带有 _id、operationType、fullDocument、ns、documentKey 等字段的变更事件文档。
逻辑时钟:ts 与集群时间的双重角色
oplog 条目的 ts 是变更流的逻辑时钟。它不只是排序依据,还承担两个职责:一是作为游标推进的“位置坐标”,二是作为恢复令牌的组成部分。与之配合的还有集群时间(clusterTime),它由主节点在每次写入时推进,并通过心跳在节点间传播,用于因果一致性会话。
需要区分两个概念:oplog ts 决定“事件在日志中的位置”,而集群时间决定“客户端会话看到的一致性快照”。变更流游标在打开时会记录一个起始位置,之后按 ts 顺序向后扫描;当客户端携带恢复令牌重连时,服务端就是靠令牌中的 ts 定位到“上次读到哪一条之后”。
恢复令牌:断点续传的凭证
每个变更事件都携带一个 _id 字段,它同时充当恢复令牌(resume token)。令牌内部封装了 ts、ns、uuid、documentKey 等信息,足以让服务端在 oplog 中精确定位。恢复令牌的语义是“我已经处理到这条事件,请从它的下一条继续”,因此它天然是“排他”的。
恢复令牌的有效性依赖 oplog 窗口。如果应用长时间停机,令牌指向的 ts 已被覆盖,重连时服务端会返回 ChangeStreamHistoryLost 错误,此时只能重新建立游标并从当前时间开始,历史事件无法补回。这是变更流工程中必须提前设计的容错点。
游标:从 oplog 到应用的事件管道
变更流游标(cursor)是客户端与服务端之间的有状态通道。它维护一个“当前位置”,服务端按批返回事件,客户端通过 getMore 持续拉取。游标支持 $match、$project、$addFields 等聚合阶段,过滤在服务端完成,减少网络传输。
游标还支持 fullDocument 策略:默认 update 事件只返回变更的字段差异(updateDescription),若需要完整文档,可设置 fullDocument: "updateLookup",服务端会额外查询当前文档。注意这带来的是“查询时刻”的文档,而非“变更时刻”的文档,二者可能不一致。
依赖关系与工程含义
把上述对象串起来,可以看到一条清晰的依赖链:Primary 写入 → oplog 条目(ts 逻辑时钟)→ 变更流游标按 ts 扫描 → 事件携带 resumeToken → 应用持久化令牌 → 重连时用令牌定位。这条链上任何一环的容量或时序假设被打破,都会直接影响变更流的可用性。
由此可以推导出几条实践准则。第一,oplog 大小必须与业务峰值写入速率和可容忍停机时间匹配。若每秒写入约 2000 条 oplog 条目、每条平均 200 字节,则每小时约产生 1.44 GB oplog;若希望容忍 4 小时停机,oplog 至少应配置到 6 GB 以上并留出余量。第二,resumeToken 必须与业务处理结果一起持久化,且要在业务处理成功之后再保存,否则会丢事件;若先保存令牌再处理,则可能重复处理,需要业务侧幂等。第三,不要依赖 fullDocument 的强一致快照,updateLookup 返回的是查询时刻文档,若业务需要变更时刻的完整状态,应在事件中携带必要字段或自行维护版本。
一个可观察的验证过程
要确认上述机制,可以在测试副本集上做一次最小验证。先查看 oplog 窗口:
rs.printReplicationInfo()
// configured oplog size: 2048 MB
// log length start to end: 3600 secs (1 hrs)
// oplog first event time: Mon Nov 20 2023 10:00:00
// oplog last event time: Mon Nov 20 2023 11:00:00
再打开一个变更流并打印事件与令牌:
const cs = db.users.watch([{ $match: { "operationType": "insert" } }]);
while (!cs.isClosed()) {
const ev = cs.tryNext();
if (ev) {
printjson({ op: ev.operationType, key: ev.documentKey, token: ev._id });
}
}
随后在另一会话插入文档,观察事件中的 _id 是否包含 ts 与 uuid;再手动停止消费者超过 oplog 窗口后重连,观察是否抛出 ChangeStreamHistoryLost。这两步能直观验证“令牌定位依赖 oplog 窗口”这一核心结论。
小结
变更流是 oplog 的读取视图,逻辑时钟 ts 决定顺序与定位,恢复令牌是断点续传的凭证,游标是承载过滤与拉取的状态通道。四者环环相扣:oplog 窗口决定可恢复范围,令牌决定从哪里恢复,游标决定以什么粒度消费。把这条依赖链理解清楚,后续设计幂等消费、监控滞后、规划 oplog 容量时,才能做出有依据的工程决策。