变更流需副本集或分片集群,单节点不支持;须初始化副本集、连接字符串加replicaSet参数;watch()返回异步迭代器,须用for await消费;需手动处理错误重连与resumeToken;建议用$project精简传输字段。

变更流需要副本集或分片集群才能启用
单节点 MongoDB 实例默认不支持变更流,db.collection.watch() 会直接报错 TopologyDescription does not contain any hosts 或更明确的 watch is not supported on standalone servers。必须先将单节点升级为副本集(哪怕只有 1 个成员),否则代码跑不通。
实操建议:
- 启动时加
--replSet rs0参数,例如:mongod --dbpath ./data --replSet rs0 --port 27017 - 连接后执行
rs.initiate()初始化副本集 - Node.js 连接字符串需包含
?replicaSet=rs0,如:mongodb://localhost:27017/mydb?replicaSet=rs0 - 若用 Docker,确保
mongod容器以副本集模式运行,且网络互通
collection.watch() 返回的是可暂停/恢复的异步迭代器
它不是一次性拉取所有变更,也不是 EventEmitter 那种监听模式,而是返回一个 ChangeStream 对象,本质是实现了 [Symbol.asyncIterator] 的异步可迭代对象。这意味着你得用 for await...of 才能持续消费事件。
常见错误是误写成 watch().on('change', ...) —— ChangeStream 没有 on 方法,也不触发 Node.js 标准事件。
正确写法示例:
const changeStream = collection.watch();
for await (const change of changeStream) {
console.log('捕获变更:', change.operationType, change.fullDocument?._id);
}
注意点:
根据 package.json 中的 engines 字段自动检测 Node.js 版本需求,未安装 nvm 时自动安装并切换至对应版本。支持 Windows、macOS、Linux。适用于 Node.js 项目开发、版本切换和环境配置。
- 一旦
for await循环退出(比如抛出未捕获异常),流就关闭,不会自动重连 - 想手动控制,可用
changeStream.close();重启需重新调用watch() - 如果要过滤特定操作,传入聚合管道数组,如
[{ $match: { 'operationType': 'insert' } }]
连接中断后默认不自动重试,需手动处理 resumetoken
网络抖动或 MongoDB 重启会导致 ChangeStream 抛出 MongoNetworkError 或 MongoCursorExhaustedError,此时流终止,且原 resumeToken 失效。不处理就会丢变更。
可靠做法是捕获错误、记录最新 token、重建流:
- 每次收到变更后,缓存
change._id(即resumeToken)到内存或 Redis - 错误回调中用该 token 启动新流:
collection.watch([{ $resumeAfter: lastToken }]) - 首次启动可用
{ $startAfter: ... }或留空从最新开始,但要注意:空参数可能跳过部分变更 - 生产环境强烈建议配合
maxAwaitTimeMS(如10000)避免长阻塞
聚合管道里用 $project 减少传输体积很关键
默认变更事件包含完整文档(fullDocument 字段)、更新描述(updateDescription)、时间戳等,数据量大时网络和解析开销明显。尤其当文档本身很大(如含 base64 图片字段),不精简会拖慢整个消费逻辑。
推荐在 watch() 第二个参数中配置聚合管道:
collection.watch(
[ { $project: { '_id': 1, 'operationType': 1, 'fullDocument.name': 1, 'fullDocument.status': 1 } } ]
)
这样只传输必要字段,fullDocument 变成只含 name 和 status 的子对象。注意:$project 不能用在 fullDocument 外层直接裁剪,必须通过点号路径指定嵌套字段。
额外提醒:如果只关心增删改类型,连 fullDocument 都不要,用 documentKey + 应用层查库更省带宽。

















