在 Node.js 中使用 MongoDB 事務

MongoDB事務

事務介紹

在 MongoDB 中,對單個文檔的操做是原子的。因爲您可使用嵌入的文檔和數組來捕獲單個文檔結構中的數據之間的關係,而不是跨多個文檔和集合進行規範化,所以這種單一文檔的原子性消除了對多文檔的需求許多實際用例的事務。node

對於須要對多個文檔(在單個或多個集合中)進行讀取和寫入原子化的狀況,MongoDB 支持多文檔事務。對於分佈式事務,事務可用於多個操做、集合、數據庫、文檔和分片。mongodb

事務和原子性

分佈式事務和多單據事務 從 MongoDB 4.2 開始,這兩個術語是同義詞。分佈式事務是指分片羣集和副本集上的多文檔交易記錄。多文檔事務(不管是在分片羣集仍是副本集上)也稱爲從 MongoDB 4.2 開始的分佈式事務。 對於須要對多個文檔(在單個或多個集合中)進行讀取和寫入原子化的狀況,MongoDB 支持多文檔事務:docker

在版本 4.0中,MongoDB 支持副本集上的多文檔事務。shell

在版本 4.2中,MongoDB 引入了分佈式事務,這增長了對分片羣集上的多文檔事務的支持,併合並了對副本集上多文檔事務的現有支持。數據庫

要在 MongoDB 4.2 部署(副本集和分片羣集)上使用事務,客戶端必須使用爲 MongoDB 4.2 更新的 MongoDB 驅動程序。api

多文檔事務是原子的(即提供"全無"命題):數組

當事務提交時,事務中所作的全部數據更改都將保存在事務外部並可見。也就是說,事務不會提交其某些更改,而回滾其餘更改。bash

在事務提交以前,事務中所作的數據更改在事務外部不可見。session

可是,當事務寫入多個分片時,並不是全部外部讀取操做都須要等待提交的事務的結果在分片中可見。例如,若是提交事務,寫入 1 在分片 A 上可見,但在分片 B 上還沒有顯示寫入 2,則讀取時的外部讀取"local"能夠讀取寫入 1 的結果,而看不到寫入 2。app

當事務停止時,事務中所作的全部數據更改將被丟棄,而不會變得可見。例如,若是事務中的任何操做失敗,事務將停止,而且事務中所作的全部數據更改將被丟棄,而不會變得可見。

準備工做

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('demo')
    .collection('cats')
    .insertOne({ name: 0 }, { w: 'majority' });

  await client
    .db('demo')
    .collection('cats')
    .insertOne({ name: 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('demo').collection('cats');
      const coll2 = client.db('demo').collection('cats');

      // 必須將會話傳遞給操做

      await coll1.insertOne({ name: 1 }, { session });
      await coll2.insertOne({ name: 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: 修改值}})
複製代碼
  • 副本集模式下,集合不會被自動建立須要手動建立集合才能進行操做
/*
2020-03-14 14:05:16,525 ERROR 2476 [-/127.0.0.1/-/30ms GET /] nodejs.Error: MongoError: Cannot create namespace demo.cats in multi-document transaction.
*/
複製代碼
擴展 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();
      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();
    }
    // 手動拋出異常後,事務回滾,查看數據庫能夠看到,插入和刪除文檔都沒有生效
    /*
    2020-03-14 14:01:38,503 ERROR 2476 [-/127.0.0.1/-/25ms GET /] nodejs.Error: InternalServerError: Internal Server Error
    */
複製代碼

相關文章
相關標籤/搜索