MongoDB事务知识点梳理
1. 事务简介
1.1 事务的背景与必要性
在MongoDB中,对单个文档的操作天生就是原子的。得益于MongoDB强大的文档模型,开发者可以将相关数据嵌入到单个文档中,从而在许多场景下避免了对跨文档事务的需求。
然而,在金融、会计等对数据一致性要求极高的场景中,应用程序可能需要对多个文档(可能跨多个集合或数据库)执行原子性读写操作。为此,MongoDB从4.0版本开始引入了多文档事务 ,并在4.2版本升级为分布式事务,使其能够运行在分片集群上。
1.2 ACID特性
MongoDB事务完全支持ACID特性,确保数据的可靠性和一致性:
- 原子性(Atomicity):事务中的所有操作要么全部成功提交,要么全部失败回滚。不存在部分成功的情况。
- 一致性(Consistency):事务开始前和结束后,数据库都处于一致的状态。如果事务中止,所有更改将被丢弃,数据回滚到事务开始前的状态。
- 隔离性(Isolation) :MongoDB默认使用快照隔离(Snapshot Isolation)。在事务提交之前,事务中所做的数据更改对事务外部是不可见的,从而避免了脏读、不可重复读等现象。
- 持久性(Durability):一旦事务提交成功,其修改的数据将被永久保存。MongoDB通过预写日志(Journal)机制确保即使在系统故障或宕机后,已提交的事务也不会丢失。
1.3 隔离级别详解
MongoDB的WiredTiger存储引擎支持多种隔离级别,默认采用快照隔离:
- 脏读:指一个事务读取了另一个未提交事务的数据。在快照隔离下,MongoDB事务只能读取已提交的数据,因此避免了脏读。
- 不可重复读:指在一个事务内,两次读取同一数据得到不同结果。由于事务基于快照读取,快照在整个事务生命周期内是一致的,因此避免了不可重复读。
- 幻读:指一个事务读取某个范围的数据时,另一个事务插入了新数据,导致再次读取时数据条数发生变化。快照隔离也能有效防止幻读。
1.4 事务与原子性的关系
| 特性 | 单文档操作 | 多文档事务 |
|---|---|---|
| 原子性范围 | 单个文档的修改 | 跨多个文档、集合、数据库的修改 |
| 适用场景 | 大多数常规读写场景 | 强一致性要求的金融、库存扣减等场景 |
| 性能开销 | 低 | 相对较高(涉及锁、两阶段提交等) |
2. 如何使用事务
2.1 前置条件与配置
在使用事务前,MongoDB环境需满足以下要求:
- 版本要求:副本集事务需MongoDB 4.0+,分片集群分布式事务需MongoDB 4.2+。
- 部署模式 :事务只能在副本集 或分片集群上使用,单机模式不支持多文档事务。
- 存储引擎:必须使用WiredTiger存储引擎。
2.2 事务API核心组件
MongoDB事务基于会话(Session) 实现。所有事务操作都必须在同一个会话中完成。
- 启动会话 :通过
startSession()创建客户端会话。 - 开启事务 :在会话中调用
startTransaction()。 - 执行操作:执行具体的CRUD操作(注意:必须将session作为参数传递给每个操作)。
- 提交/回滚 :所有操作成功则调用
commitTransaction()提交;捕获异常则调用abortTransaction()回滚。
2.3 完整事务示例
mongo Shell 示例:
javascript
// 1. 开启会话
session = db.getMongo().startSession();
// 2. 获取集合(通过会话获取)
coll1 = session.getDatabase("mydb1").foo;
coll2 = session.getDatabase("mydb2").bar;
// 3. 开启事务,并设置读/写关注
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
// 4. 执行事务内的操作
coll1.insertOne({ abc: 1 });
coll2.insertOne({ xyz: 999 });
// 5. 提交事务
session.commitTransaction();
print("事务提交成功");
} catch (error) {
// 6. 出错则中止事务
session.abortTransaction();
print("事务回滚:" + error);
} finally {
session.endSession();
}
回调API(推荐) :
现代驱动通常提供 withTransaction 回调方法,自动处理提交和重试逻辑:
javascript
const session = client.startSession();
const callback = async (session) => {
const coll1 = client.db("mydb1").collection("foo");
const coll2 = client.db("mydb2").collection("bar");
await coll1.insertOne({ abc: 1 }, { session });
await coll2.insertOne({ xyz: 999 }, { session });
};
try {
await session.withTransaction(callback);
// 事务成功提交
} catch (error) {
// 事务最终失败
} finally {
await session.endSession();
}
2.4 事务中的支持操作
MongoDB事务支持大部分CRUD操作,但存在一些限制:
| 支持的操作 | 不支持的操作 |
|---|---|
insert / update / delete |
写入 system.*、config、admin、local 集合 |
find 查询 |
写入 Capped Collection(固定大小集合) |
aggregate(聚合) |
explain 命令 |
findAndModify |
listCollections / listIndexes 命令 |
| 在事务内创建集合/索引(MongoDB 4.4+) | 在跨分片写事务中创建新集合 |
3. 对应用程序的事务限制进行调优
3.1 运行时限制与超时
- 事务超时 :默认情况下,MongoDB会自动中止运行时间超过 60秒 的事务(参数:
transactionLifetimeLimitSeconds)。 - 锁获取超时 :事务在等待获取锁时,如果等待超过 5毫秒 (参数:
maxTransactionLockRequestTimeoutMillis),事务将被终止。 - 优化建议:将大型事务拆分为多个小型事务,并确保查询使用了合适的索引,以缩短执行时间。
3.2 大小限制与文档数量
| 限制类型 | MongoDB 4.0 | MongoDB 4.2+ | 推荐实践 |
|---|---|---|---|
| 事务日志大小 | 单个oplog条目限制 16MB | 支持多个oplog条目,无硬性限制 | 控制事务大小在 16MB 以内 |
| 修改文档数 | 无硬性限制 | 无硬性限制 | 建议每个事务修改文档数不超过 1000 个 |
3.3 并发控制与写冲突(Write Conflicts)
当多个事务同时尝试修改同一个文档时,会发生写冲突。MongoDB通过MVCC机制处理并发:
- 乐观锁机制 :事务开始时获取数据的快照版本,提交时检查版本是否变更。如果版本已变,提交会失败并抛出
WriteConflict错误。 - 重试机制 :对于临时性冲突,驱动会自动重试事务(使用
withTransactionAPI 时)。 - 最佳实践:尽量避免在事务内外同时修改同一文档;减少事务的持有时间,以降低冲突概率。
3.4 读/写关注点优化
合理设置读关注(Read Concern)和写关注(Write Concern)可以平衡性能与一致性:
| 关注点类型 | 常用级别 | 说明 |
|---|---|---|
| 读关注 | "local" |
默认值,性能最好,可能读到被回滚的数据 |
"majority" |
只读已提交的持久化数据,保证一致性 | |
"snapshot" |
事务级别使用,确保从快照中读取,避免幻读 | |
| 写关注 | { w: 1 } |
主节点确认即可,性能较高 |
{ w: "majority" } |
大多数节点确认,保证持久性,事务建议使用此级别 |
3.5 常见问题与规避措施
- 长事务导致缓存压力:长事务会阻止WiredTiger缓存中的旧版本数据被释放,可能导致缓存压力过大甚至"死锁"。应尽早提交或回滚事务。
- DDL阻塞事务:在事务中尽量避免执行DDL操作(如创建索引),此类操作会获取全局锁,阻塞其他事务。
- 跨分片事务开销:跨分片的事务(分布式事务)性能开销较大。在数据建模时,尽量将频繁参与事务的文档放在同一个分片上。
3.6 性能调优清单
- 数据建模:优先使用嵌入式文档,减少对事务的依赖。
- 索引优化:确保事务中的查询拥有合适的索引,避免全表扫描。
- 事务大小:控制修改文档数量在1000以内,oplog大小在16MB以内。
- 超时设置 :根据业务场景调整
transactionLifetimeLimitSeconds参数。 - 客户端重试 :编写完善的异常捕获和重试逻辑(尤其是
TransientTransactionError)。 - 版本选择:如果重度使用事务,建议升级至 MongoDB 5.0+ 以获取更多稳定性优化和内核缺陷修复。