MongoDB has atomicity in single-document operations, not in multi-document operations, and usually requires transactions to implement ACID properties.

Introduction to transaction APIS

The MongoDB Client Driver provides corresponding API interfaces for transaction operations of clients. MongoDB does not support transactions until 4.0, and the client driver version should be selected accordingly.

This article uses MongoDB Client Driver version 3.5

Session Session

Session is a concept introduced after MongoDB 3.6. In previous versions, each request in the Mongod process creates an OperationContext, which can be understood as a single-line transaction. Changes to data, indexes, and Oplog in this single-row transaction are atomic.

Sessions after MongoDB 3.6 are also essentially a context in which multiple requests share a context, providing the basis for multiple document transaction implementations.

Why does db.col.count () often fail after a crash?

The reason is that the number of table records is updated independently of the data update transaction, see article mongoing.com/archives/54… .

The transaction function

  • startTransaction()

Start a new transaction, after which CRUD operations can be performed.

  • commitTransaction()

Commit transactions hold data, and the data changed in the transaction is not visible to the outside world until commit.

  • abortTransaction()

Transaction rollback, for example, if a portion of data fails to be updated, the modified data is also rolled back.

  • endSession()

End the session.

Simple implementation in Mongo Shell

var session = db.getMongo().startSession();
session.startTransaction({readConcern: { level: 'majority' },writeConcern: { w: 'majority' }});
var coll = session.getDatabase('test').getCollection('user');

coll.update({name: 'Jack'}, {$set: {age: 18}})

// Successfully committed the transaction
session.commitTransaction();

// Failed transaction rollback
session.abortTransaction();
Copy the code

MongoDB transaction practices in Nodejs

To better understand how MongoDB transactions are used in Node.js, give an example.

Suppose we now have a shopping mall order scenario, divided into an item table (store item data, inventory information) and an order table (store order records). Before placing an order, check whether the inventory is greater than 0. If the inventory is greater than 0, deduct the commodity inventory and create an order; otherwise, it will prompt that the inventory is insufficient and the order cannot be placed.

The data model

// goods
{
    "_id": ObjectId("5e3b839ec2d95bfeecaad6b8"),
    "goodId":"g1000".Id / / commodities
    "name":"Test Commodity 1".// Product name
    "stock":2.// Inventory of goods
    "price":100 // Amount of goods
}
/ / db. Goods. Insert ({" goodId ":" g1000 ", "name" : "test item 1", "stock" : 2, "price" : 100})
Copy the code
// order_goods
{
    "_id":ObjectId("5e3b8401c2d95bfeecaad6b9"),
    "id":"o10000"./ / order id
    "goodId":"g1000".// The product Id corresponding to the order
    "price":100 // Order amount
}
// db.order_goods.insert({ id: "o10000", goodId: "g1000", price: 100 })
Copy the code

Node.js operates the MongoDB native API implementation

Note: readPreference must be set to the primary node, not the secondary node, in a transaction.

db.js

Link to MongoDB and initialize an instance.

const MongoClient = require('mongodb').MongoClient;
const dbConnectionUrl = 'mongo: / / 192.168.6.131:27017192168 6.131:27018192168 6.131:27019 /? replicaSet=May&readPreference=secondaryPreferred';
const client = new MongoClient(dbConnectionUrl, {
  useUnifiedTopology: true});let instance = null;

module.exports = {
  dbInstance: async() = > {if (instance) {
      return instance;
    }

    try {
      instance = await client.connect();
    } catch(err) {
      console.log(`[MongoDB connection] ERROR: ${err}`);
      throw err;
    }

    process.on('exit', () => {
      instance.close();
    });

    returninstance; }};Copy the code

index.js

const db = require('./db');

const testTransaction = async (goodId) => {
  const client = await db.dbInstance();
  const transactionOptions = {
    readConcern: { level: 'majority' },
    writeConcern: { w: 'majority' },
    readPreference: 'primary'};const session = client.startSession();
  console.log('Transaction status:', session.transaction.state);

  try {
    session.startTransaction(transactionOptions);
    console.log('Transaction status:', session.transaction.state);

    const goodsColl = await client.db('test').collection('goods');
    const orderGoodsColl = await client.db('test').collection('order_goods');
    const { stock, price } = await goodsColl.findOne({ goodId }, { session });
    
    console.log('Transaction status:', session.transaction.state);
    
    if (stock <= 0) {
        throw new Error('Understock');
    }

    await goodsColl.updateOne({ goodId }, {
        $inc: { stock: - 1 } // Inventory minus 1
    })
    await orderGoodsColl.insertOne({ id: Math.floor(Math.random() * 1000),  goodId, price  }, { session });
    await session.commitTransaction();
  } catch(err) {
    console.log(`[MongoDB transaction] ERROR: ${err}`);
    await session.abortTransaction();
  } finally {
    await session.endSession();
    console.log('Transaction status:', session.transaction.state);
  }
}

testTransaction('g1000')
Copy the code

Run the test

View the current transaction state after each transaction function execution.

Node index Transaction status: NO_TRANSACTION Transaction status: STARTING_TRANSACTION Transaction status: TRANSACTION_IN_PROGRESS Transaction status: TRANSACTION_COMMITTEDCopy the code