这是《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() } }
}
)
设计建议:
- 把强一致的小边界放进同一文档;
- 用条件更新实现状态机;
- 用
$inc更新计数; - 用版本号乐观并发;
- 避免读改写竞争。
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 事务要求
多文档事务要求:
- 副本集或分片集群;
- 使用会话;
- 数据和目录在支持事务的存储引擎;
- 分片集群需要可路由的分片键或目标操作;
- 事务超时和锁资源可控。
单机 mongod 不应承载生产多文档事务。
19.4 事务边界
适合事务:
- 转账;
- 订单与库存强一致变更;
- 多表状态联动;
- 配置组变更;
- 少量跨集合写入。
不适合事务:
- 调用外部 HTTP;
- 大批量导入;
- 长时间计算;
- 等待用户输入;
- 跨多个微服务的最终一致流程。
外部 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
检查:
- 每个事务内更新条件;
- 唯一索引;
- 分片键;
- 执行计划;
- 冲突率。
19.8 分片事务
跨 shard 事务比单 shard 事务成本更高:
- 参与者更多;
- 协调时间更长;
- 网络故障影响更大;
- 需要提交协议;
- 更容易超时。
设计建议:
- 事务涉及的文档尽量同 shard;
- 复合分片键覆盖事务实体;
- 跨 shard 事务有限使用;
- 监控事务时长;
- 长流程改最终一致。
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
告警:
- 事务时长 P99 超标;
- 冲突率升高;
- 回滚率升高;
- 当前事务数持续增长;
- 锁等待集中。
本章小结
MongoDB 的第一选择是利用单文档原子性和条件更新。多文档事务适合少量强一致操作,必须短小、有索引、明确读写关注并处理重试。长流程和跨服务流程应使用状态机、补偿和事件驱动。
思考题
- 为什么单文档更新更适合状态机?
- 多文档事务中为什么不能调用外部 HTTP?
- snapshot 读关注解决什么问题?
- 哪些错误可以安全重试事务?
- 分片事务为什么要减少跨 shard?