change stream 在 mongodb 5.0 副本集上可用,但需满足 majority 读写关注且客户端连主节点;db.watch() 比手动 aggregate($changestream) 更适合副本集,因其自动处理主切与重连,并支持 startafter 断点续传。

Change Stream 在 MongoDB 5.0 副本集上可用,但必须满足两个硬性前提:副本集必须启用 majority 读写关注(默认开启),且客户端连接必须指向主节点或使用支持读取偏好 primary 的驱动配置。直接连 secondary 并调用 $changeStream 会报错 CommandNotSupportedOnSecondary。
为什么 db.watch() 比 $changeStream 更适合副本集场景
在副本集而非分片集群中,db.watch() 是更简洁、容错更强的入口。它底层自动处理主节点切换和重连逻辑,而手写 aggregate([{$changeStream: {}}]) 需要自己监听 invalidate 事件并重建游标。
-
db.watch()可以传入resumeAfter或startAfter令牌,其中startAfter支持在invalidate后恢复(resumeAfter不支持) -
db.watch()默认监听整个数据库下所有非system集合;若只关心某集合,应改用db.collection.watch() - 聚合管道中不能把
$changeStream放在第二阶段——它必须是第一个阶段,否则报错ChangeStreamStageMustBeFirst
如何正确构造包含过滤逻辑的 Change Stream 管道
变更流的过滤必须通过聚合管道后续阶段完成,不能在 $changeStream 阶段内写条件。例如想只监听 status === "active" 的更新,得用 $match 接在后面:
db.users.watch([
{ $changeStream: { fullDocument: "updateLookup" } },
{ $match: { "fullDocument.status": "active" } }
])
注意几点:
-
$match必须作用于变更事件结构,不是原始文档字段。常见字段有:operationType("insert"/"update"/"delete")、fullDocument、updateDescription - 如果用了
fullDocument: "updateLookup",fullDocument字段才存在;否则只有documentKey和变更描述 - 不能在
$match中引用_id字段做修改(MongoDB 4.2+ 限制),否则整个管道失败并抛出ChangeStreamPipelineModifiesIdField
resumeAfter 和 startAfter 的实际区别与选型
两者都用于断点续传,但行为关键不同:
-
resumeAfter依赖上次事件的_id字段值(即恢复令牌),但它**无法跨主节点失效(invalidate)后继续**——一旦发生主从切换导致流中断,该令牌就作废 -
startAfter同样接受恢复令牌,但允许你在invalidate事件后新建一个流并从中断点继续,更健壮 - 二者互斥,且都不能和
startAtOperationTime共存;生产环境建议优先用startAfter,尤其在副本集主节点可能频繁切换时
获取恢复令牌的方式是每次处理完事件后缓存 event._id,不要试图解析或修改它——它是二进制安全的 opaque token。
最易被忽略的一点:Change Stream 的 resume 机制不保证“不丢不重”,它只保证“至少一次”交付。如果你的应用需要精确一次语义(exactly-once),必须在应用层做幂等控制,比如用 event._id 做去重缓存,或结合业务唯一键做 upsert。











