跳到正文

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 事务的对比 ​

维度MongoDBMySQL InnoDB
默认隔离级别snapshot(事务内)REPEATABLE READ(InnoDB 默认,不是 RC)
锁粒度文档级 + 意向锁行级 + gap lock
跨节点事务4.2+ 原生两阶段提交XA / 应用层补偿
事务超时60s(transactionLifetimeLimitSeconds)innodb_lock_wait_timeout 默认 50s
DDL4.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。

把一门技术讲透到既懂也会用