一、变更流基础概念与运行环境
1.什么是 Change Streams
Change Streams 是 MongoDB 在 3.6 版本引入的一种持久化变更数据捕获(change data capture,CDC)机制,允许应用程序以实时、顺序的方式监听集合、数据库或整个部署上发生的数据变更事件。它基于复制集的操作日志(oplog)构建,保证顺序性、持久性且支持断点续传。
变更事件包括:
insert:插入新文档update:更新现有文档replace:替换整个文档delete:删除文档- 此外还有
invalidate、drop、rename等 DDL 相关事件
2.必须运行在复制集环境
Change Streams 要求目标部署为副本集(Replica Set)或分片集群(Sharded Cluster,实际上每个分片都是副本集)。单节点 mongod 无法使用变更流,因为单节点不产生 oplog,而 Change Streams 将 oplog 的变更记录流式暴露给应用。
3.watch() 方法入门
在驱动中调用 collection.watch()、db.watch() 或 MongoClient.watch() 即可启动一个变更流游标:
javascript
// 1. 监听整个部署(所有库、所有表)
const clientChangeStream = client.watch();
// 2. 监听指定数据库(例如 orderDB 下的所有表)
const db = client.db('orderDB');
const dbChangeStream = db.watch();
// 3. 监听指定数据库中的指定集合(例如 orderDB 库下的 orders 表)
const collection = db.collection('orders');
const collChangeStream = collection.watch();
游标返回的事件对象结构如下(简化):
json
{
"_id": { "_data": "..." }, // resume token
"operationType": "insert",
"fullDocument": { ... },
"ns": { "db": "...", "coll": "..." },
"documentKey": { "_id": ... },
"clusterTime": Timestamp(...)
}
二、核心工作流程与架构
1.从 oplog 到 Change Stream
下图展示了一个副本集中变更流的数据流动路径:

- 写入操作在主节点上先写入数据文件,然后写入一条 oplog 条目。
- 变更流通过维护一个不断推进的游标,持续获取 oplog 中特定集合或数据库的新条目。
- 游标内部记录了一个 resume token,用于持久化消费位点,支持故障恢复。
2.事件顺序保证
Change Streams 保证事件按全局排序的 clusterTime 顺序交付,并且 total ordering 在单个分片集合上成立。对于分片集群,跨分片的事件顺序是因果一致的,但不保证全局绝对时序。
三、事件类型详解与返回字段
下表对比四种主要 DML 事件的特征和返回字段:
| 事件类型 | 发生场景 | fullDocument 字段 |
updateDescription 字段 |
documentKey 字段 |
特殊说明 |
|---|---|---|---|---|---|
insert |
新文档插入 | 包含完整新文档 | 不存在 | 新文档的 _id |
- |
update |
修改字段(如 $set) |
默认不出现;开启 fullDocument: 'updateLookup' 时返回更新后完整文档 |
包含 updatedFields 和 removedFields |
被更新文档的 _id |
默认仅返回增量字段,降低传输开销 |
replace |
完整替换文档 | 包含替换后的新文档 | 不存在 | 被替换文档的 _id |
- |
delete |
删除文档 | 不存在 | 不存在 | 被删文档的 _id |
documentKey 仅为 _id,无法获取删除前内容 |
重要限制 :对于 update 事件,如果未指定 fullDocument: 'updateLookup',则事件中不包含完整的文档快照,仅列出哪些字段被修改。若需要更新后的完整文档用于下游处理,必须显式开启该选项。
四、配置选项与高级用法
下表对关键配置选项进行对比:
| 选项 | 作用 | 典型值 | 适用场景 |
|---|---|---|---|
fullDocument |
控制返回的完整文档内容 | 'default'(不返回),'updateLookup'(更新后),'whenAvailable'(分片下可能为空) |
需要更新后完整文档的场景 |
resumeAfter |
指定某个操作之后开始监听 | 一个 resume token | 精确断点续传 |
startAtOperationTime |
根据时间戳开始监听,仅近似定位 | Timestamp | 容错启动,不要求精确 token |
startAfter |
在指定 resume token 之后开始监听 | resume token | 避免重复消费 |
batchSize |
游标批量拉取数量 | 整数 | 控制吞吐与延迟 |
maxAwaitTimeMS |
在空游标时等待新事件的最长时间 | 毫秒数 | 实时性与轮询开销的平衡 |
它们属于 MongoDB 客户端代码(应用程序代码) 中的配置参数。
具体来说,它们是你在编写应用程序(如 Java, Python,Node.js 等)调用 MongoDB 驱动,或者在 mongosh 中监听 Change Stream(变更流)时,作为方法参数或选项对象传入的。
bash
// 直接在 mongosh 命令行中传入参数
db.collection.watch([], { fullDocument: 'updateLookup', batchSize: 50 })
1.断点续传(resume token)
每个变更事件都携带一个全局唯一的 _id 字段,即 resume token。应用程序必须持久化该 token(保存到数据库或文件),在连接中断或重启后使用 resumeAfter 重新创建变更流,继续从上次位置消费,保证不丢数据。
五、应用场景与限制
1.典型应用场景
- 缓存同步:当源数据集发生变更,实时更新 Redis 等缓存层。
- 搜索引擎数据同步:将 MongoDB 数据变更推送到 Elasticsearch、Solr 等搜索引擎。
- 微服务事件总线:将数据变更作为事件广播给下游微服务,实现最终一致性。
- 实时通知与审计:捕获敏感数据变更并记录审计日志,或推送通知。
- 数据仓库 ETL:增量抽取数据到数据湖或数仓。
2.关键限制与避坑指南
- 无法回看历史变更:变更流只反映开启监听之后发生的变化,无法回溯到开启前已发生的操作。
- 更新事件默认只传增量 :如果下游需要完整的文档,必须设置
fullDocument: 'updateLookup',但会增加一次额外的文档查找,略微影响性能。 - 资源占用与心跳:变更流游标会占用服务器资源,长时间闲置的游标会被服务端自动关闭(默认 10 分钟无活动)。应用应实现重连与 token 恢复逻辑。
- 集合删除与重命名 :如果集合被删除或重命名,该集合上的变更流会收到
invalidate事件并关闭,需要重新监听新集合。 - 分片集群注意事项:分片键字段缺失时,变更事件可能不含完整分片键,某些操作可能无法正确路由。
六、实操练习:使用 Docker 搭建单节点副本集与 Node.js 监听
1.使用 Docker 搭建单节点副本集
1.创建 docker-compose.yml 文件
yaml
version: '3.8'
services:
mongo-rs0:
image: mongo:7.0
container_name: mongodb-rs0
restart: always
ports:
- "27017:27017"
command: mongod --replSet rs0 --bind_ip_all
volumes:
- ./mongo-data:/data/db
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh --quiet
interval: 10s
timeout: 5s
retries: 5
2.启动容器并初始化副本集
bash
# 在 docker-compose.yml 所在目录执行
docker-compose up -d
# 等待容器启动完成(约 5 秒),然后进入容器执行初始化
docker exec -it mongodb-rs0 mongosh --eval "rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'localhost:27017' }] })"
若初始化成功,会输出类似 { ok: 1 } 的响应。
说明 :在 Docker 内部网络中,
localhost:27017指向容器自身,因此初始化时可以直接使用该地址。如果你的客户端需要从宿主机外部连接,连接字符串应使用localhost:27017(已通过端口映射暴露)。
3. 验证副本集状态
bash
docker exec -it mongodb-rs0 mongosh --eval "rs.status()"
看到 "stateStr" : "PRIMARY" 即表示单节点副本集已正常运行。
4. 创建练习数据库与集合
bash
docker exec -it mongodb-rs0 mongosh --eval "
use testDB;
db.createCollection('orders');
"
此时,一个完全可用的单节点副本集已在 Docker 中就绪,支持 Change Streams 及事务操作。
2.Node.js 监听 orders 变更并模拟通知
以下代码使用 MongoDB Node.js 原生驱动(版本 ≥ 4.0)。实现功能:监听 orders 集合的 insert 事件,打印订单信息并模拟发送通知。
javascript
const { MongoClient } = require('mongodb');
async function main() {
const uri = 'mongodb://localhost:27017/?replicaSet=rs0';
const client = new MongoClient(uri);
try {
await client.connect();
const db = client.db('testDB');
const orders = db.collection('orders');
// 开启 change stream,过滤只监听 insert 事件
const pipeline = [{ $match: { operationType: 'insert' } }];
const changeStream = orders.watch(pipeline, {
fullDocument: 'updateLookup' // 但对 insert 其实默认就有全文档,保留作为示范
});
console.log('开始监听 orders 集合的 insert 事件...');
// 消费变更流
while (true) {
if (changeStream.closed) {
console.log('变更流已关闭,尝试重新连接...');
// 在实际生产环境中,应使用保存的 resume token 恢复
break;
}
const next = await changeStream.next();
const newOrder = next.fullDocument;
console.log('--------------------');
console.log('新订单插入:', JSON.stringify(newOrder, null, 2));
// 模拟发送通知(实际可接入邮件、短信、消息队列)
simulateNotification(newOrder);
}
} catch (err) {
console.error('错误:', err);
} finally {
await client.close();
}
}
function simulateNotification(order) {
console.log(`[通知] 订单 ${order._id} 已创建,金额: ${order.total}`);
// 此处替换为真实通知逻辑
}
main().catch(console.error);
将以上代码复制并保存到一个名为 watcher.js(或任何你喜欢的 .js 结尾的名字)的文件中。
在 Linux 命令行中,使用 node 命令来执行这个脚本:
bash
# 初始化一个 Node.js 项目(生成 package.json 文件)
npm init -y
# 本地安装 mongodb 驱动
npm install mongodb
# 执行脚本
node watcher.js
运行方式 :启动脚本后,在 mongosh 中插入一条文档测试:
javascript
db.orders.insertOne({ _id: 1, product: "Laptop", total: 999 })
Node.js 控制台将输出订单内容及模拟通知。
3.实操流程可视化

七、常见面试题
问题 1 :MongoDB Change Streams 依赖什么机制实现?为什么单机 mongod 不能使用?
答:依赖复制集的操作日志(oplog)实现。oplog 记录了所有数据变更,Change Streams 通过持续读取 oplog 生成事件流。单机 mongod 没有复制集,不维护 oplog,因此无法使用变更流。
问题 2 :update 事件中,为什么有时拿不到更新后的完整文档?如何获取完整文档?
答 :为了降低网络和 IO 开销,默认 update 事件只返回增量变更(updateDescription),不包含完整文档。设置 fullDocument: 'updateLookup' 后,Change Stream 会通过文档 _id 回查主节点,返回更新后的完整文档,但会引入一次额外的查询。
问题 3 :应用重启后如何保证 Change Streams 不丢消息?
答 :应用需要持久化每个事件中的 _id 字段,即 resume token。重启时使用 resumeAfter 选项传入该 token,变更流将从该位置之后继续推送新事件,实现精确续传。如果不保存 token,可使用 startAtOperationTime 大致定位时间点,但可能会重复或遗漏少量事件。
问题 4 :Change Streams 与传统的消息队列(如 Kafka)在数据同步场景下有何主要区别?
答:Change Streams 是数据库内建的变更捕获机制,直接绑定 MongoDB 的复制协议,无需引入外部中间件,运维简单、顺序性强,且天然支持 resume token 恢复。但它是数据库主动推送的模式,消费者必须直接连接数据库,缺乏消息队列的持久化和多消费者组扩展能力。消息队列更适用于解耦、缓冲和多消费者订阅的场景。两者常结合使用:Change Streams 做源头捕获,推送到 Kafka,再由多个服务消费。
问题 5 :分片集群中使用 Change Streams,有哪些需要注意的地方?
答:分片集群中 Change Stream 返回的事件来自多个分片,事件顺序是因果一致的,但不保证全局精确时序。跨分片读写可能产生乱序。此外,每个分片上的 oplog 独立,总连接数会增加。需要确保应用中使用的分片键字段在变更事件中存在,否则某些操作的路由信息可能不完整。