MongoDBNotes

第 19 章:事务

zjc 于 2026-01-19 发布

这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 MongoDB 支持单文档原子操作和多文档 ACID 事务。单文档原子性是重要优势,多文档事务则适合跨集合或跨文档的一致性边界,但会占用资源、增加冲突,应保持短小。

19.1 单文档原子性

以下更新在单个文档内原子完成:

db.orders.updateOne(
  { _id: "o_10001", status: "WAIT_PAY" },
  {
    $set: { status: "PAID" },
    $inc: { version: 1 },
    $push: { status_history: { status: "PAID", at: new Date() } }
  }
)

设计建议:

  1. 把强一致的小边界放进同一文档;
  2. 用条件更新实现状态机;
  3. $inc 更新计数;
  4. 用版本号乐观并发;
  5. 避免读改写竞争。

19.2 多文档事务

示例:

const session = db.getMongo().startSession();

try {
  session.startTransaction({
    readConcern: { level: "snapshot" },
    writeConcern: { w: "majority" }
  });

  const orders = session.getDatabase("shop").orders;
  const accounts = session.getDatabase("shop").accounts;

  orders.updateOne(
    { _id: "o_10001", status: "WAIT_PAY" },
    { $set: { status: "PAID" } }
  );

  accounts.updateOne(
    { _id: "a_1001", balance: { $gte: NumberDecimal("100.00") } },
    { $inc: { balance: NumberDecimal("-100.00") } }
  );

  session.commitTransaction();
} catch (error) {
  session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

19.3 事务要求

多文档事务要求:

  1. 副本集或分片集群;
  2. 使用会话;
  3. 数据和目录在支持事务的存储引擎;
  4. 分片集群需要可路由的分片键或目标操作;
  5. 事务超时和锁资源可控。

单机 mongod 不应承载生产多文档事务。

19.4 事务边界

适合事务:

  1. 转账;
  2. 订单与库存强一致变更;
  3. 多表状态联动;
  4. 配置组变更;
  5. 少量跨集合写入。

不适合事务:

  1. 调用外部 HTTP;
  2. 大批量导入;
  3. 长时间计算;
  4. 等待用户输入;
  5. 跨多个微服务的最终一致流程。

外部 IO 不应放在数据库事务内。

19.5 读关注与写关注

事务示例:

session.startTransaction({
  readConcern: { level: "snapshot" },
  writeConcern: { w: "majority", wtimeout: 5000 }
})

组合建议:

场景 组合
交易核心 snapshot + majority
普通业务 snapshot 或 local + majority
内部任务 local + w:1
对账任务 snapshot

一致性越强,故障和网络异常下的等待成本越高。

19.6 重试与错误

常见错误类型:

错误 处理
TransientTransactionError 可重开事务
UnknownTransactionCommitResult 可重试提交
写冲突 重试或调整模型
超时 拆小事务
分片路由错误 检查分片键

推荐驱动使用事务 API 的便捷重试能力;手写时必须区分事务重试和提交重试。

19.7 事务与索引

事务内查询也要有索引:

filter without index
  -> scan many documents
  -> hold resources longer
  -> more conflicts

检查:

  1. 每个事务内更新条件;
  2. 唯一索引;
  3. 分片键;
  4. 执行计划;
  5. 冲突率。

19.8 分片事务

跨 shard 事务比单 shard 事务成本更高:

  1. 参与者更多;
  2. 协调时间更长;
  3. 网络故障影响更大;
  4. 需要提交协议;
  5. 更容易超时。

设计建议:

  1. 事务涉及的文档尽量同 shard;
  2. 复合分片键覆盖事务实体;
  3. 跨 shard 事务有限使用;
  4. 监控事务时长;
  5. 长流程改最终一致。

19.9 替代方案

不是所有一致性问题都要用事务:

需求 方案
单对象状态流转 条件更新
重复请求 唯一索引
计数 $inc
跨服务长流程 Saga + 补偿
搜索索引同步 Change Stream + 幂等
对账 离线核对

事务适合短小一致边界,不适合长生命周期业务流程。

19.10 监控指标

transactions_total
transactions_commit_total
transactions_abort_total
transaction_duration_ms
transaction_write_conflicts_total
transactions_current
transaction_open_cursors
lock_queue_time_ms

告警:

  1. 事务时长 P99 超标;
  2. 冲突率升高;
  3. 回滚率升高;
  4. 当前事务数持续增长;
  5. 锁等待集中。

本章小结

MongoDB 的第一选择是利用单文档原子性和条件更新。多文档事务适合少量强一致操作,必须短小、有索引、明确读写关注并处理重试。长流程和跨服务流程应使用状态机、补偿和事件驱动。

思考题

  1. 为什么单文档更新更适合状态机?
  2. 多文档事务中为什么不能调用外部 HTTP?
  3. snapshot 读关注解决什么问题?
  4. 哪些错误可以安全重试事务?
  5. 分片事务为什么要减少跨 shard?