一、介绍¶
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。
为什么不建议使用?
事务 = 锁,节点协调,额外开销,性能影响。
通过合理地设计文档模型,可以规避绝大部分使用事务的必要性
二、MongoDB ACID 多文档事务支持¶
| 事务属性 | 支持程度 |
|---|---|
| Atomocity 原子性 | 单表单文档:1.x 就支持;复制集多表多行:4.0 复制集;分片集群多表多行:4.2 |
| Consistency 一致性 | writeConcern, readConcern (3.2) |
| Isolation 隔离性 | readConcern (3.2) |
| Durability 持久性 | Journal and Replication |
三、使用方法¶
MongoDB 多文档事务的使用方式与关系数据库非常相似:
try (ClientSession clientSession = client.startSession()) {
clientSession.startTransaction();
collection.insertOne(clientSession, docOne);
collection.insertOne(clientSession, docTwo);
clientSession.commitTransaction();
}
四、事务的隔离级别¶
- 事务完成前,事务外的操作对该事务所做的修改不可访问
- 如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read
五、实验:启用事务后的隔离性¶
repl:PRIMARY> db.tx.insertMany([{ x: 1 }, { x: 2 }]);
repl:PRIMARY> db.tx.find()
{ "_id" : ObjectId("635bc84764d7be2f932c3a0d"), "x" : 1 }
{ "_id" : ObjectId("635bc84764d7be2f932c3a0e"), "x" : 2 }
repl:PRIMARY> var session = db.getMongo().startSession();
repl:PRIMARY> session.startTransaction();
repl:PRIMARY> var coll = session.getDatabase('test').getCollection("tx");
repl:PRIMARY> coll.updateOne({x: 1}, {$set: {y: 1}}); //事务内操作将 x=1改为 y=1
repl:PRIMARY> coll.find() //事务内查询
{ "_id" : ObjectId("635bc84764d7be2f932c3a0d"), "x" : 1, "y" : 1 }
{ "_id" : ObjectId("635bc84764d7be2f932c3a0e"), "x" : 2 }
repl:PRIMARY> db.tx.find() //事务外查询
{ "_id" : ObjectId("635bc84764d7be2f932c3a0d"), "x" : 1 }
{ "_id" : ObjectId("635bc84764d7be2f932c3a0e"), "x" : 2 }
repl:PRIMARY> session.commitTransaction(); //提交事务(或者 s.abortTransaction()回滚事务)
repl:PRIMARY> db.tx.find()
{ "_id" : ObjectId("635bc9e964d7be2f932c3a0f"), "x" : 1, "y" : 1 }
{ "_id" : ObjectId("635bc9e964d7be2f932c3a10"), "x" : 2 }
六、实验:可重复读 Repeatable Read¶
db.tx.insertMany([{ x: 1 }, { x: 2 }]);
db.tx.find()
{ "_id" : ObjectId("635bc84764d7be2f932c3a0d"), "x" : 1 }
{ "_id" : ObjectId("635bc84764d7be2f932c3a0e"), "x" : 2 }
var session = db.getMongo().startSession();
session.startTransaction({readConcern: {level: "snapshot"},writeConcern: {w: "majority"}});
var coll = session.getDatabase('test').getCollection("tx");
coll.findOne({x: 1}); // 事务内,返回: {x: 1}
{ "_id" : ObjectId("635bcc1164d7be2f932c3a11"), "x" : 1 }
db.tx.updateOne({x: 1}, {$set: {y: 1}}); //事务外更新
db.tx.findOne({x: 1}); // 事务外,返回: {x: 1, y: 1}
{ "_id" : ObjectId("635bcc1164d7be2f932c3a11"), "x" : 1, "y" : 1 }
coll.findOne({x: 1}); // 事务内,返回: {x: 1}
{ "_id" : ObjectId("635bcc1164d7be2f932c3a11"), "x" : 1 }
session.commitTransaction(); // 提交
db.tx.findOne({x: 1});
{ "_id" : ObjectId("635bcc1164d7be2f932c3a11"), "x" : 1, "y" : 1 } //这是比较高的一个事务隔离性
七、事务写机制¶
MongoDB 的事务错误处理机制不同于关系数据库:
- 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个文档时会触发 Abort 错误,因为此时的修改冲突了;
- 这种情况下,只需要简单地重做事务就可以了;
- 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行
写冲突实验:
(1) 实验1,2个窗口测试事务内的更新
--继续使用上个实验的 tx 集合,开两个 mongo shell 均执行下述语句
var session = db.getMongo().startSession();
session.startTransaction({ readConcern: {level: "snapshot"},writeConcern: {w: "majority"}});
var coll = session.getDatabase('test').getCollection("tx");

(2) 实验2,事务外更新
窗口1:第一个事务,正常提交
coll.updateOne({x: 1}, {$set: {y: 1}});
窗口2:另一个事务更新同一条数据,异常
coll.updateOne({x: 1}, {$set: {y: 2}});
窗口3:事务外更新,需等待
db.tx.updateOne({x: 1}, {$set: {y: 3}});
八、注意事项¶
- 可以实现和关系型数据库类似的事务场景
- 必须使用与 MongoDB 4.2 兼容的驱动;
- 事务默认必须在 60 秒(可调)内完成,否则将被取消;
- 涉及事务的分片不能使用仲裁节点;
- 事务会影响 chunk 迁移效率。正在迁移的 chunk 也可能造成事务提交失败(重试即可);
- 多文档事务中的读操作必须使用主节点读;
- readConcern 只应该在事务级别设置,不能设置在每次读写操作上。
- 必须是 WT 引擎才支持事务。