一、介绍

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 引擎才支持事务。