生态集成 — CodeRoadMap生态集成
这一课你将能做到
- 用 Change Streams 监听 MongoDB 数据变更,并说明 resume token 和容错机制
- 解释 MongoDB Connector for Kafka 的双向同步逻辑,以及 MongoDB + Elasticsearch 的索引同步策略
- 判断一个场景适合 Change Streams、Kafka Connector 还是定时批量同步
为什么要学
MongoDB 不是孤岛:用户提交代码后,需要把事件发给 Kafka → 消费服务生成排行榜;搜索需要把课程标题同步到 Elasticsearch;运营后台需要把行为日志流进数据仓库。这些链路都依赖「数据从 MongoDB 流出去」或「数据从外部流进 MongoDB」的桥接能力。
Change Streams 是「MongoDB 原生的 push 流」,Kafka Connector 是「企业级桥梁」,ES 同步是「搜索引擎补强」。这一课把三者讲清楚,让你知道哪个场景用哪把刀。
先建立图像
三种集成模式:
① Change Streams(本课重点)
MongoDB 写入
│
oplog ──> Change Stream 监听 ──> 应用消费
│ (resume token 断点续传)
典型:站内消息推送、实时通知、缓存失效
② Kafka Connector(CDC:Change Data Capture)
MongoDB ──> Source Connector ──> Kafka Topic ──> 消费方
消费方 ──> Sink Connector ──> MongoDB(反向写回)
典型:跨系统事件总线、数据仓库同步
③ ES 同步(搜索增强)
MongoDB 写入 ──> 应用双写或 connector ──> Elasticsearch
查询走 ES,详情走 MongoDB
典型:站内搜索、全文检索、聚合分析
跟着做
步骤 1:Change Streams 基础监听
条件:必须是复制集或分片集群(单机无 oplog,不支持)。
// mongosh 里直接监听
db.submissions.watch([]).on("change", doc => printjson(doc))
// 用特定过滤:只监听 accepted 状态的新插入
const pipeline = [
{ $match: { "fullDocument.status": "accepted", operationType: "insert" } }
]
db.submissions.watch(pipeline).on("change", doc => {
print(`新提交: user=${doc.fullDocument.userId} lesson=${doc.fullDocument.lessonSlug}`)
})
插入一条测试:
db.submissions.insertOne({ userId: 1024, lessonSlug: "mdbrm-integrate", status: "accepted" })
Change Stream 立即收到事件:operationType: "insert",ns 是库名.集合名,fullDocument 是插入的完整文档。
步骤 2:Resume Token —— 断点续传
// 启动时记录 token,崩溃后从 token 恢复
const watcher = db.submissions.watch([])
watcher.on("change", doc => {
const token = doc._id // resume token
// 持久化到 Redis / 文件 / 另一个集合
db.resumeTokens.updateOne({ name: "submissions" }, { $set: { token } }, { upsert: true })
})
// 恢复时读取持久化的 token
const saved = db.resumeTokens.findOne({ name: "submissions" })
const resumeAfter = saved ? { _data: saved.token._data } : undefined
db.submissions.watch([], { resumeAfter })
没有 resume token,重启后只能从最新开始,中间的变更会漏掉。
步骤 3:Change Streams 的事件类型
db.submissions.watch([]).on("change", doc => {
switch (doc.operationType) {
case "insert": /* doc.fullDocument */ break;
case "update": /* doc.updateDescription.updatedFields, removedFields */ break;
case "delete": /* doc.documentKey (仅 _id) */ break;
case "replace": /* doc.fullDocument */ break;
}
})
注意 update 事件默认只带变更的字段(updateDescription),需要完整文档要加 fullDocument: "updateLookup":
db.submissions.watch([], { fullDocument: "updateLookup" })
步骤 4:Java 驱动监听
MongoCollection<Document> subs = db.getCollection("submissions");
List<Bson> pipeline = List.of(
Aggregates.match(Filters.in("operationType", List.of("insert", "update")))
);
MongoChangeStreamCursor<ChangeStreamDocument<Document>> cursor = subs.watch(pipeline).cursor();
while (cursor.hasNext()) {
ChangeStreamDocument<Document> change = cursor.next();
System.out.println(change.getOperationType() + " " + change.getFullDocument());
}
生产里把循环跑在独立线程或 Spring 的 @EventListener + @Async 中,注意 resume token 持久化。
步骤 5:MongoDB → Kafka(Source Connector)
概念:Confluent / MongoDB 官方提供 Kafka Connect 插件,把 MongoDB 的 oplog 事件翻译成 Kafka 消息。
{
"name": "mongo-source-submissions",
"config": {
"connector.class": "com.mongodb.kafka.connect.MongoSourceConnector",
"connection.uri": "mongodb://m1:27017,m2:27017,m3:27017/codeforge?replicaSet=rs0",
"database": "codeforge",
"collection": "submissions",
"topics.prefix": "mongo",
"change.stream.full.document": "updateLookup",
"output.json.formatter": "com.mongodb.kafka.connect.source.json.formatter.SimplifiedJson"
}
}
Kafka 消费者收到 mongo.codeforge.submissions topic 的消息,处理排行榜更新、风控检测等。
步骤 6:Kafka → MongoDB(Sink Connector)
{
"name": "mongo-sink-reviews",
"config": {
"connector.class": "com.mongodb.kafka.connect.MongoSinkConnector",
"connection.uri": "mongodb://m1:27017,m2:27017,m3:27017/codeforge?replicaSet=rs0",
"database": "codeforge",
"collection": "reviews",
"topics": "review-events",
"document.id.strategy": "com.mongodb.kafka.connect.sink.processor.id.strategy.PartialValueStrategy",
"document.id.strategy.partial.value.projection.list": "submissionId"
}
}
步骤 7:MongoDB → Elasticsearch 同步策略
- 应用双写:写 MongoDB 后同步写 ES,最简单但不可靠(MongoDB 写成功、ES 写失败会不一致)。加本地事务(Spring
@Transactional)仍不能保证跨系统。
- Change Streams 双写:用 Change Streams 监听到变更后写 ES。可靠性好,需自写消费者(和步骤 4 类似)。
- 第三方工具:Mongo-Connector(已停止维护)、Monstache(Go 写的实时同步)、Logstash 定时拉取。生产推荐 Monstache 或自研 Change Streams 消费者。
// Change Streams 消费者里写 ES 的伪代码
for await (const change of db.submissions.watch([], { fullDocument: "updateLookup" })) {
if (change.operationType === "delete") {
await esClient.delete({ index: "submissions", id: change.documentKey._id.toHexString() })
} else {
await esClient.index({
index: "submissions",
id: change.fullDocument._id.toHexString(),
document: { ... }
})
}
}
讲清楚要点
- Change Streams 是拉还是推?
底层是轮询 oplog 的「拉」,但对应用暴露的是事件驱动 API(cursor 阻塞直到有数据),所以体验像推。不消耗 MongoDB 的锁或日志。
- resume token 的本质:一个指向 oplog 位置的标记。持久化它就能实现「断点续传」。token 有过期时间(oplog 窗口),太久没消费 resume 会失败。
- oplog 窗口 = 消费延迟的生死线:如果 Change Streams 消费者挂了,重启时 oplog 里那段已经过期(被新写覆盖),resume 会报错,只能从最新开始——对数据不丢有要求的场景,oplog 要配得够大(7.x 无固定大小限制,由磁盘空间决定)。
- Change Streams vs Kafka Connector:前者是「MongoDB 原生的轻量流」,适合「我要在应用层立刻做点什么」;后者是「跨系统的总线」,适合多下游消费、削峰填谷、异构系统集成。
- ES 同步不选定时扫描:几十万集合上定时
find() 扫全表是性能灾难,写放大且延迟不可控。实时同步必须用流式方案(Change Streams / Monstache)。
- Change Streams 过滤在管道里做:
$match 尽量推到 Change Stream pipeline 里,减少事件传输量。但复杂过滤会让 resume token 的处理变复杂——简单场景用管道,复杂场景消费端过滤。
- 多库/多集合监听:MongoDB 支持
db.watch()(库级)和 client.watch()(全局),pipeline 里用 ns 过滤区分来源集合。
你可能会懵的地方
- Change Streams 能监听 delete 吗?
能,但 delete 事件默认只给
documentKey(_id),因为 delete 时文档已不存在。需要完整文档得在应用层预存一份(或在删除前先发一次事件),或用 fullDocumentBeforeChange(企业版/Atlas 功能)。
- 多消费者怎么分摊?
Kafka 天然消费组;Change Streams 没有分区消费概念,每个 watcher 是独立连接,多消费者就是多 watcher。要分摊需自行按文档分片(如
_id % N 做 pipeline 过滤)。
- Change Streams 对性能有影响吗?
极小。它读的是已有 oplog,不是额外表扫描;但持久 resume token 和
fullDocument: "updateLookup" 需要额外一次文档查询,更新频繁时有一定开销。
- Kafka Connector 和 Change Streams 选哪个?
只有一个下游、低延迟 → Change Streams 直接消费;多个下游、需要持久缓冲/重放 → Kafka 中转。两者可以串:MongoDB → Kafka Connector → Kafka → 自建消费者,发挥 Kafka 的分发能力。
- oplog 大小怎么监控?
rs.printReplicationInfo() 看 oplog 窗口时长;Change Streams 消费者要保证消费延迟远小于窗口时长,否则断点续传失效。
错法对照
| 错法 | 问题 | 正解 |
|---|
| Change Streams 不持久 resume token | 重启就漏事件 | 每收到事件立刻持久化 token |
| 用定时扫描替代流式同步 | 慢、延迟高、写放大 | Change Streams 或 Kafka CDC |
| 单机 MongoDB 上开 Change Streams | 单机无 oplog,不支持 | 至少起 3 节点复制集 |
| Change Streams 消费者抛异常就退出 | 不再监听,事件堆积 | try/catch 重试 + 告警 + 死信队列 |
全局 client.watch() 不加 $match | 所有库所有集合的事件全进来 | pipeline 里按 ns.db/ns.coll 过滤 |
卡点急救
- resume token 失效报错:消费者停摆太久,oplog 窗口已滚动。无药可救,只能从最新开始;重要事件要另加消息队列做兜底(双写 Kafka)。
- Change Streams 收不到事件:检查复制集是否就绪;确认 pipeline 过滤条件没写反(如匹配了
status: "accepted" 但新插入是 "pending");检查 cursor 未被意外关闭。
- Kafka Source Connector 消费慢:调大
change.stream.full.document 级别(不要用 updateLookup 全量拉,按需选 default/updateLookup),或增加 connector 任务数(tasks.max)。
面试常问
- MongoDB 如何实现实时同步到 Elasticsearch?
用 Change Streams 监听数据变更,事件经过过滤/转换后写入 ES;resume token 持久化防丢。不要定时全量扫描。
- Change Streams 和触发器有什么区别?
Change Streams 是应用侧的异步监听,不阻塞写;触发器(Triggers,Atlas 功能)可在 MongoDB 侧执行代码,但耦合度高。原生 Change Streams 更通用。
- Kafka Connector 的 exactly-once 语义怎么保证?
Source Connector 使用 resume token 记录消费位点,Kafka 端有 offset;Sink 使用幂等写(按
_id 覆盖)。MongoDB 原生写入是幂等的(_id 唯一),天然满足至少一次即有效。
自测三问
- Change Streams 的 resume token 有什么作用?丢了会怎样?
- oplog 窗口和消费延迟的关系是什么?
- 「提交记录变更后实时更新排行榜」适合 Change Streams 直连还是 Kafka Connector?为什么?
小结一句话
Change Streams 是 MongoDB 的「血管」——让数据流进事件总线、搜索引擎、缓存和一切下游;记得带好 resume token 这根「输液管」,断了就没法续。
延伸