主题
MongoDB 事务与 Change Stream
一句话:多文档事务把「几条写要么全成、要么全败」变成数据库能力,Change Stream 把 oplog 变成可订阅的变更流;前者用锁和两阶段提交换原子性,后者用 oplog 窗口换实时性,两者都有明确边界。
适用版本:事务 4.0(副本集)/ 4.2(分片);Change Streams 3.6+;下文以 MongoDB 7.0 / 8.0 为准。
为什么需要
单文档更新本身原子,但业务经常要求「扣款 + 记流水」「订单 + 库存」一起成功。没有事务时,应用要自己做补偿、幂等和失败重试,逻辑很快失控。Change Stream 解决另一个问题:下游(缓存、搜索、数仓)需要知道数据变了,轮询既慢又漏,直接读 oplog 又太底层。
MongoDB 的答案是:事务把多条写包成一个会话级原子单元;Change Stream 在 oplog 之上提供带 resume token 的游标,让应用像消费消息一样消费变更。
核心机制
事务的版本与边界
| 版本 | 能力 |
|---|---|
| 3.x | 只有单文档原子操作 |
| 4.0 | 副本集内多文档事务 |
| 4.2 | 分片集群跨分片事务(两阶段提交) |
| 4.4+ | 事务内可创建集合/索引(跨分片写事务除外;显式创建要求 readConcern:"local") |
事务默认隔离级别是 snapshot:事务开始时拿一个快照,事务内所有读看到同一版本;写冲突在文档级检测,冲突时后写的事务失败,需要应用重试。
关键限制:
transactionLifetimeLimitSeconds默认 60 秒,超时自动 abort。- 没有「最多 1000 个文档」的硬限制;真正的约束是单条 oplog entry 不超过 16MB,以及事务持有的锁和内存。4.2 起一个事务可以产生多条 oplog entry。
- 跨分片事务由 mongos 协调,走两阶段提交:各 shard 先 prepare,协调者收到全部 prepared 后再 commit;延迟和锁持有时间都明显高于单分片事务。
- 事务内不要做大批量导入、长循环或外部调用;事务越长,冲突和超时概率越高。
与 MySQL 事务的对比
| 维度 | MongoDB | MySQL InnoDB |
|---|---|---|
| 默认隔离级别 | snapshot(事务内) | REPEATABLE READ(InnoDB 默认,不是 RC) |
| 锁粒度 | 文档级 + 意向锁 | 行级 + gap lock |
| 跨节点事务 | 4.2+ 原生两阶段提交 | XA / 应用层补偿 |
| 事务超时 | 60s(transactionLifetimeLimitSeconds) | innodb_lock_wait_timeout 默认 50s |
| DDL | 4.4+ 有限支持,跨分片仍受限 | 事务内 DDL 能力更强 |
| 适用 | 少量文档的原子写 | 成熟的事务型业务 |
MySQL 默认是 RR 这一点经常被写反:InnoDB 的默认隔离级别从 5.x 到 8.0 都是 REPEATABLE READ,Read Committed 需要显式设置。
Change Stream 的数据源与 API
Change Stream 建立在 oplog 之上,只能在副本集或分片集群使用,单机 mongod 不支持。它按集合、数据库或客户端三个层级打开:
javascript
// 集合级
db.orders.watch(pipeline, options)
// 数据库级
db.watch(pipeline, options)
// 集群级(驱动里的 MongoClient)
client.watch(pipeline, options)常见选项:
| 选项 | 作用 | 版本 |
|---|---|---|
fullDocument: "updateLookup" | update 事件回查当前完整文档 | 3.6+ |
fullDocument: "whenAvailable"/"required" | 查不到时的行为 | 6.0+ |
fullDocumentBeforeChange | 返回变更前文档 | 6.0+ |
resumeAfter | 从 token 之后继续 | 3.6+ |
startAfter | 从 token 之后继续,容忍 token 略旧 | 5.0+ |
showExpandedEvents | 增加 create/drop/modify 等事件 | 6.0+ |
每个事件都带 _id(resume token)、operationType、ns、documentKey;insert/replace 带 fullDocument,update 默认只带 updateDescription,需要完整文档就开 updateLookup。
Resume Token 与 oplog 窗口
resume token 本质是 oplog 中的位置。消费者必须持久化 token,重启或网络闪断后用 resumeAfter/startAfter 续读。token 的有效期取决于 oplog 窗口:oplog 被覆盖后,旧 token 失效,只能全量重刷。
oplog 默认大小:Linux 64 位约为磁盘的 5%、最少 990MB;macOS/Windows 为 192MB(以官方文档为准)。生产上按「写入速率 × 最长消费中断时间 × 2」估算,并监控 rs.printReplicationInfo() 的时间窗口。
最小上手
javascript
// mongosh:事务
const session = db.getMongo().startSession()
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
})
try {
const accounts = session.getDatabase("bank").accounts
accounts.updateOne({ _id: "A", balance: { $gte: 100 } },
{ $inc: { balance: -100 } })
accounts.updateOne({ _id: "B" }, { $inc: { balance: 100 } })
session.commitTransaction()
} catch (e) {
session.abortTransaction()
throw e
} finally {
session.endSession()
}javascript
// Node.js 驱动:集合级 Change Stream
const stream = db.collection("orders").watch(
[{ $match: { operationType: { $in: ["insert", "update", "replace", "delete"] } } }],
{ fullDocument: "updateLookup" }
)
for await (const event of stream) {
await handle(event) // 先处理
await saveToken(event._id) // 再持久化 token;顺序反过来会丢事件
}生产动作
- 事务范围最小化:只包必须原子的几条写;批量数据用
bulkWrite,不要塞进一个事务。 - 重试:捕获
TransientTransactionError重跑整个事务;提交阶段捕获UnknownTransactionCommitResult重试 commit。 - 写关注与读偏好:事务内写用
w:"majority",读偏好固定primary;不要在事务里读 secondary。 - Change Stream 消费:先落业务结果,再保存 token(或把两者放进同一事务/幂等键);保证至少一次消费、业务侧幂等。
- oplog 容量:
oplogSizeMB按写入速率和最长中断时间复核;lag 和 oplog 窗口都要告警,不能只看节点是否在线。 updateLookup成本:update 事件回查文档会增加读放大;只需要变更字段时不要开。
边界
- 事务不是 ETL 工具:大批量、长事务会持锁、放大 oplog、增加冲突;导入用批量写 + 幂等。
- 跨分片事务有明确代价:两阶段提交、网络往返、锁等待;数据模型应尽量让相关文档落在同一 shard。
- Change Stream 不是持久化消息队列:事件只保留在 oplog 窗口内;需要长期保留、多消费组、重放,选 Kafka。
- 单机不支持 Change Stream:必须副本集或分片集群。
- 顺序只在单分片/单集合内保证:跨 shard 没有全局顺序,不要拿时间戳假设全序。
讲清楚
MongoDB 的事务从 4.0 开始支持副本集,4.2 支持跨分片,默认是快照隔离,写冲突在文档级检测,冲突后应用重试。默认 60 秒超时,没有 1000 个文档的硬限制,但单条 oplog entry 不能超过 16MB,跨分片事务还要走两阶段提交,所以事务要短、要小。对比 MySQL 时注意:InnoDB 默认是 REPEATABLE READ,不是 Read Committed。
Change Stream 是 oplog 之上的订阅接口,分集合、数据库、客户端三级,事件带 resume token。生产上最关键的是两件事:token 要在业务处理之后持久化,oplog 窗口要覆盖最长消费中断。它适合轻量 CDC 和缓存同步;要长期保留、多消费组和重放,还是用 Kafka。