Использование транзакций MongoDB в Node.js

Node.js

транзакции MongoDB

Введение в бизнес

В MongoDB операции с одним документом атомарны. Эта атомарность одного документа устраняет необходимость в нескольких документах, поскольку вы можете использовать встроенные документы и массивы для захвата отношений между данными в рамках одной структуры документа, а не для нормализации нескольких документов и коллекций. Транзакции варианта использования.

MongoDB поддерживает транзакции с несколькими документами для случаев, когда чтение и запись в несколько документов (в одной или нескольких коллекциях) должны быть атомарными. Для распределенных транзакций транзакции могут использоваться для нескольких операций, коллекций, баз данных, документов и сегментов.

Транзакции и атомарность

Распределенные транзакции и многодокументные транзакции Начиная с MongoDB 4.2, эти два термина являются синонимами. Распределенные транзакции относятся к транзакциям с несколькими документами в сегментированных кластерах и наборах реплик. Транзакции с несколькими документами (будь то в сегментированных кластерах или наборах реплик) также известны как распределенные транзакции, начиная с MongoDB 4.2. MongoDB поддерживает многодокументные транзакции для ситуаций, когда атомарные операции чтения и записи требуются для нескольких документов (в одной или нескольких коллекциях):

В версии 4.0 MongoDB поддерживает многодокументные транзакции в наборах реплик.

В версии 4.2 MongoDB представила распределенные транзакции, которые добавили поддержку транзакций с несколькими документами в сегментированных кластерах и объединили с существующей поддержкой транзакций с несколькими документами в наборах реплик.

Чтобы использовать транзакции в развертываниях MongoDB 4.2 (наборы реплик и сегментированные кластеры), клиенты должны использовать драйвер MongoDB, обновленный для MongoDB 4.2.

Транзакции с несколькими документами являются атомарными (т. е. предоставляют предложение «ничего»):

Когда транзакция фиксируется, все изменения данных, сделанные в транзакции, сохраняются и видны вне транзакции. То есть транзакция не фиксирует одни свои изменения и откатывает другие.

Изменения данных, внесенные в транзакцию, не видны за пределами транзакции, пока транзакция не будет зафиксирована.

Однако, когда транзакция записывает в несколько сегментов, не все внешние операции чтения должны ждать, пока результат зафиксированной транзакции станет видимым в сегментах. Например, если транзакция зафиксирована, запись 1 видна на осколке А, но запись 2 еще не видна на осколке Б, внешнее чтение «локальное» при чтении может прочитать результат записи 1, при этом видя меньше, чем запись 2. .

Когда транзакция прерывается, все изменения данных, сделанные в рамках транзакции, отбрасываются, не становясь видимыми. Например, если какая-либо операция в транзакции завершается сбоем, транзакция прерывается, и все изменения данных, сделанные в транзакции, отбрасываются, не становясь видимыми.

Готов к работе

Предпосылка MongoDB с использованием транзакций заключается в том, что версия MongoDB выше 4.0, а рабочий режим MongoDB необходимо настроить как набор реплик.Одного узла MongoDB недостаточно для поддержки транзакций, поскольку для транзакций MongoDB требуется как минимум два узла. Один из них — главный узел, отвечающий за обработку клиентских запросов, а остальные — подчиненные узлы, отвечающие за репликацию данных на главном узле. Общие методы коллокации узлов mongodb: один главный и один подчиненный, один главный и несколько подчиненных. Главный узел записывает на нем все оплоги операций, а подчиненный узел периодически опрашивает главный узел, чтобы получить эти операции, а затем выполняет эти операции над своей собственной копией данных, тем самым обеспечивая согласованность данных подчиненного узла с данными главного узла.

развертывать Версия совместимости функций
набор реплик 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)
        }
    }
}

контейнер
Развертывание докера
//指定 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
剩余配置方法与命令行部署相同

Использование транзакций MongoDB в Node.js

Использование драйвера 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(); }

скопировать код

Использование mongoose для выполнения транзакций в среде Egg.js
Вопросы, требующие внимания См. (статья блогера блог-сада)
  • Вам нужно использовать mongoose.connection для выполнения транзакционных операций над коллекцией.Методы CRUD других моделей не поддерживают транзакции
mongoose.connection.collection('集合名') // 注:集合名需要小写且加s,如model为Cat,集合名这里应写为cats

  • Для запуска определяемых схемой значений промежуточного программного обеспечения по умолчанию требуется создание экземпляров модели.
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 напрямую вставляет часть данных, определенное значение по умолчанию не будет активировано, например, созданное поле, соответствующее поле типа: Схема .ObjectId, определенный в chema, 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: 修改值}})

расширить контекст
// /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'} 的记录
c6171450a3663e7fe9d77919ea7fd0ab.png
c6171450a3663e7fe9d77919ea7fd0ab.png
откат транзакции
    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();
    }
    // 手动抛出异常后,事务回滚,查看数据库可以看到,插入和删除文档都没有生效
d31f585a5cd2097cc0bbb3bb9c35f788.png
d31f585a5cd2097cc0bbb3bb9c35f788.png

В этой статье используетсяmdniceнабор текста