MongoDB 支持事务吗?单文档原子性和多文档事务怎么理解?
简化版
MongoDB 单个文档内的写操作是原子的,这也是嵌入式建模的重要基础。MongoDB 也支持多文档事务,可以在多个文档、集合甚至分片上实现 ACID 事务。但多文档事务成本更高,不应该把 MongoDB 当成传统关系库一样到处依赖跨表事务,建模时仍应优先让经常一起更新的数据落在同一文档内。
详细版
事务相关重点:
- 单文档写入天然原子;
- 嵌入文档可以把一组强一致字段放在同一文档中;
- 多文档事务适合确实需要跨文档一致的场景;
- 事务会占用资源,增加锁、日志、复制和重试成本;
- 长事务会影响性能和并发;
- 应用要处理事务提交失败和重试。
例如订单创建同时扣减余额,如果必须强一致,可以使用事务。但很多场景也可以通过单文档设计、幂等操作、事件最终一致来降低事务范围。
完整版教学
一、MongoDB 的默认强项是单文档原子性
MongoDB 对单个文档的写操作是原子的。即使更新文档中的多个字段,也不会出现只改了一半的状态。
例如:
db.accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: -100 }, $set: { updatedAt: new Date() } }
)
这个文档内的修改要么都成功,要么都不成功。
二、嵌入设计可以减少事务需求
如果一个订单的多个明细项总是和订单一起创建、一起读取,可以嵌在订单文档里。这样更新订单状态和明细状态可能仍然是单文档操作。
这就是 MongoDB 建模和事务的关系:合理文档边界可以把很多关系库里的多表事务变成单文档原子更新。
三、多文档事务什么时候用
多文档事务适合:
- 转账类强一致操作;
- 多集合必须同时成功或失败;
- 迁移过程中短期保持关系模型;
- 关键业务不允许最终一致窗口;
- 分片环境下跨文档一致更新。
示例思路:
const session = client.startSession();
session.startTransaction();
try {
await accounts.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session });
await accounts.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session });
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
throw e;
}
真实项目还要处理重试和异常分类。
四、多文档事务不是免费午餐
事务会增加:
- 资源占用;
- 锁和冲突概率;
- oplog 和复制压力;
- 提交延迟;
- 应用重试复杂度。
如果每个请求都开启大事务,MongoDB 的文档模型优势会被削弱。
五、事务和最终一致怎么取舍
有些业务不一定需要强事务。例如下单成功后异步增加积分,可以用消息和补偿保证最终一致;订单支付和库存扣减则可能要求更强控制。
判断标准:
- 数据短暂不一致是否可接受?
- 是否能用幂等和补偿修复?
- 是否涉及金额、库存、资格等强约束?
- 是否能通过单文档建模降低一致性范围?
六、事务代码还要处理提交不确定和重试
多文档事务不是写完 commitTransaction() 就万事大吉。网络抖动、主从切换、写冲突都可能让应用遇到可重试错误或提交结果不确定。工程上要按驱动建议处理事务重试,并保证业务操作本身幂等。
startTransaction
-> 多个读写操作
-> commit
-> 成功: 返回
-> 可重试错误: 重试事务或提交
-> 不确定: 查询业务结果或按幂等键确认
比如转账请求要有 transferId 幂等号。即使客户端重试事务,也不能重复扣款;事务保证数据库层原子性,幂等设计保证应用层重复提交不会变成重复业务效果。
七、常见误区与追问
| 方案 | 一致性能力 | 成本 |
|---|---|---|
| 单文档原子更新 | 单文档内强原子 | 成本低,建模要求高 |
| 多文档事务 | 跨文档 ACID | 延迟、锁、重试成本更高 |
| 最终一致 | 通过消息和补偿收敛 | 有短暂不一致窗口 |
记忆钩子:MongoDB 先靠文档边界减少事务,再用多文档事务兜住少数强一致场景。
- 误区:MongoDB 不支持事务。 MongoDB 支持多文档事务,但它不是文档模型的默认设计重心。
- 误区:支持事务后就可以照搬关系型数据库建模。 大量跨集合事务会削弱文档模型优势,并增加锁、复制和重试成本。
- 误区:单文档原子性只能更新一个字段。 单文档内多个字段、嵌套字段和数组更新可以作为一个原子写操作提交。
- 追问:为什么嵌入设计能减少事务? 经常一起更新的小聚合对象放在同一文档中,很多操作就变成单文档原子更新。
- 追问:哪些场景适合多文档事务? 转账、库存和订单强一致、跨集合必须同时成功失败的关键链路更适合。
- 追问:事务失败后怎么处理? 区分可重试错误、提交不确定和业务失败,配合幂等键、查询确认和重试策略处理。
八、加强记忆
MongoDB 事务要先记住“单文档原子性是基础,多文档事务是补充”。能通过嵌入设计解决的,不要扩大成跨集合事务;确实需要强一致时再使用事务,并准备好处理性能成本和重试逻辑。