MongoDB事务
事务介绍
在 MongoDB 中,对单个文档的操作是原子的。由于您可以使用嵌入的文档和数组来捕获单个文档结构中的数据之间的关系,而不是跨多个文档和集合进行规范化,因此这种单一文档的原子性消除了对多文档的需求许多实际用例的事务。
对于需要对多个文档(在单个或多个集合中)进行读取和写入原子化的情况,MongoDB 支持多文档事务。对于分布式事务,事务可用于多个操作、集合、数据库、文档和分片。
事务和原子性
分布式事务和多单据事务 从 MongoDB 4.2 开始,这两个术语是同义词。分布式事务是指分片群集和副本集上的多文档交易记录。多文档事务(无论是在分片群集还是副本集上)也称为从 MongoDB 4.2 开始的分布式事务。 对于需要对多个文档(在单个或多个集合中)进行读取和写入原子化的情况,MongoDB 支持多文档事务:
在版本 4.0中,MongoDB 支持副本集上的多文档事务。
在版本 4.2中,MongoDB 引入了分布式事务,这增加了对分片群集上的多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
要在 MongoDB 4.2 部署(副本集和分片群集)上使用事务,客户端必须使用为 MongoDB 4.2 更新的 MongoDB 驱动程序。
多文档事务是原子的(即提供"全无"命题):
当事务提交时,事务中所做的所有数据更改都将保存在事务外部并可见。也就是说,事务不会提交其某些更改,而回滚其他更改。
在事务提交之前,事务中所做的数据更改在事务外部不可见。
但是,当事务写入多个分片时,并非所有外部读取操作都需要等待提交的事务的结果在分片中可见。例如,如果提交事务,写入 1 在分片 A 上可见,但在分片 B 上尚未显示写入 2,则读取时的外部读取"local"可以读取写入 1 的结果,而看不到写入 2。
当事务中止时,事务中所做的所有数据更改将被丢弃,而不会变得可见。例如,如果事务中的任何操作失败,事务将中止,并且事务中所做的所有数据更改将被丢弃,而不会变得可见。
准备工作
MongoDB 使用事务的前提是 MongoDB 版本大于 4.0,需要配置 MongoDB 工作模式为副本集,单个 MongoDB 节点不足支持事务,因为 MongoDB 事务至少需要两个节点。其中一个是主节点,负责处理客户端请求,其余的都是从节点,负责复制主节点上的数据。mongodb各个节点常见的搭配方式为:一主一从、一主多从。主节点记录在其上的所有操作oplog,从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。
部署 | 功能 兼容性版本 |
---|---|
副本集 | 4.0 |
分片集群 | 4.2 |
命令行部署
启动实例
mongod --replSet rs --dbpath=磁盘目录 --port=27017
mongod --replSet rs --dbpath=磁盘目录 --port=37017
Mongo shell
$mongo --port=27017
MongoDB shell version v4.2.3 connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb Implicit session: session { "id" : UUID("b0a2609c-6aa1-466a-849f-ba0e9f5e3d3a") } MongoDB server version: 4.2.3 ...
副本集配置
var config={ _id:"rs", members:[ {_id:0,host:"127.0.0.1:27017"}, {_id:1,host:"127.0.0.1:37017"}, ]}; rs.initiate(config) // 成功后会返回类似如下信息 { "ok" : 1, "operationTime" : Timestamp(1522810920, 1), "$clusterTime" : { "clusterTime" : Timestamp(1522810920, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } } }
容器
Docker 部署
//指定 MongoDB 版本 > 4.0,也可以指定latest
docker pull mongo:4.2.3
启动 Docker 容器
docker run --name m0 -p 37017:27017 -d mongo:4.2.3 --replSet "rs"
docker run --name m0 -p 47017:27017 -d mongo:4.2.3 --replSet "rs"
docker run --name m0 -p 57017:27017 -d mongo:4.2.3 --replSet "rs"
mongo shell
// 先进入 Docker 容器交互模式
docker exec -it CONTAINERID /bin/bash
剩余配置方法与命令行部署相同
Node.js 中使用 MongoDB事务
使用MongoDB驱动
// 对于副本集来说 Uri 中需要包含副本集名称,和成员 URI // const uri = 'mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/?replicaSet=myRepl' // 对于分片集群,连接到mongo集群实例 // const uri = 'mongodb://mongos0.example.com:27017,mongos1.example.com:27017/'
const client = new MongoClient(uri); await client.connect();
await client .db('mydb1') .collection('foo') .insertOne({ abc: 0 }, { w: 'majority' });
await client .db('mydb2') .collection('bar') .insertOne({ xyz: 0 }, { w: 'majority' });
// 第一步 启动 session,事务的所有操作都基于 session const session = client.startSession();
// 第二步 定义事务选项 const transactionOptions = { readPreference: 'primary', readConcern: { level: 'local' }, writeConcern: { w: 'majority' } };
// 第三步:使用withTransaction启动事务、执行回调和提交
try { await session.withTransaction(async () => { 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 }); }, transactionOptions);
} finally { await session.endSession(); await client.close(); }
Egg.js 框架中使用 mongoose 执行事务
注意事项 参考了(博客园博主兜兜的文章)
- 需使用mongoose.connection对集合进行事务操作,其他model的CRUD部分方法不支持事务
mongoose.connection.collection('集合名') // 注:集合名需要小写且加s,如model为Cat,集合名这里应写为cats
- 触发Schema定义的中间件默认值需要构造model实例
const CatSchema = new Schema({ name: { type: String default: 'cat' }, created: { type: Date, default: Date.now } })
const Cat = mongoose.model('Cat', CatSchema)
new Cat() // 触发中间件
- insertOne,findOneAndUpdate等方法对数据的新增,需上面第二点进行依赖,否则直接insertOne 插入一条数据,定义的默认值不会触发,如created字段,chema内部定义的type: Schema.ObjectId的相应字段,insertOne插入后都会变成字符串类型,不是Schema.ObjectId类型
// 解决方式 //新增
const Cat= new Cat(); const data = {name: 5} for (let key in data) { Cat[key] = data[key]; } db.collection('cats').insertOne(Cat);
// 查询修改
db.collection('cats') .findOneAndUpdate({_id: mongoose.Types.ObjectId(你的id)}, {$set: {name: 修改值}})
扩展 context
// /app/extend/context.js module.exports = { async getSession(opt = { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, }) { const { mongoose } = this.app; const session = await mongoose.startSession(opt); await session.startTransaction(); return session; }, };
模型
'use strict'; module.exports = app => { const CatSchema = new app.mongoose.Schema({ name: { type: String, default: 'cat', }, pass: { type: String, default: 'cat', }, created: { type: Date, default: Date.now, }, });
const Cat = app.mongoose.model('Cat', CatSchema);
new Cat(); // 触发中间件 return Cat; };
执行事务
const { mongoose } = this.ctx.app;
const session = await this.ctx.getSession();
const db = mongoose.connection;
try {
let data = { name : 'ceshi' };
const Cat = new this.ctx.model.Cat();
for (let key in data) {
Cat[key] = data[key]
}
await db
.collection('cats')
.insertOne(Cat, { session });
// 提交事务
await session.commitTransaction();
return 'ok';
} catch (err) {
// 回滚事务
const res = await session.abortTransaction();
console.log(res)
this.ctx.logger.error(new Error(err));
} finally {
await session.endSession();
}
// 执行后,数据库中多了一条 { name: 'ceshi'} 的记录
事务回滚
const { mongoose } = this.ctx.app;
const session = await this.ctx.getSession();
const db = mongoose.connection;
try {
let data = { name : 'ceshi' };
const Cat = new this.ctx.model.Cat();
for (let key in data) {
Cat[key] = data[key]
}
await db
.collection('cats')
.insertOne(Cat, { session });
await this.ctx.model.Cat.deleteMany({ name: 'ceshi' }, { session });
// 手动抛出异常
await this.ctx.throw();
// 提交事务
await session.commitTransaction();
return 'ok';
} catch (err) {
// 回滚事务
await session.abortTransaction();
this.ctx.logger.error(new Error(err));
} finally {
// 结束事务
await session.endSession();
}
// 手动抛出异常后,事务回滚,查看数据库可以看到,插入和删除文档都没有生效
本文使用 mdnice 排版