preface

I believe that friends who have used mainstream relational databases will not be too unfamiliar with “Transactions”. It allows us to integrate multiple database operations on multiple tables into an atomic operation, which can ensure non-interference between multiple data operations in high concurrency scenarios. And if an error occurs in any of these operations, the transaction is aborted and the data is rolled back, which ensures data consistency when changing data in multiple tables at the same time.

Previously, MongoDB did not support transactions, so developers had to borrow other tools to make up for the database at the business code level when they needed to use transactions. With the release of version 4.0, MongoDB has also brought native transactions to MongoDB, so let’s take a look at it and see how to use it with a simple example.

introduce

Transactions and Replica Sets

Replica set is a master/slave node architecture of MongoDB, which maximizes the availability of data and prevents the whole service from being inaccessible due to a single point of failure. Currently, MongoDB only supports multi-table transactions running on replica sets. If you want to install and run replica sets in a local environment, you can use a toolkit — run-RS. The following article provides detailed instructions:

Thecodebarbarian.com/introducing…

Transactions and Sessions

Transactions are associated with Sessions. Only one transaction can be started for a session at a time. When a session is disconnected, the transaction in that session is terminated.

Functions in transactions

  • Session.startTransaction()

Start a transaction in the current session, and data manipulation can begin once the transaction is started. Data operations performed within a transaction are isolated, meaning that operations within a transaction are atomic.

  • Session.commitTransaction()

Commits a transaction, saves the changes made to the data in the transaction, and then terminates the current transaction. The data operations of a transaction before committing are not visible to the public.

  • Session.abortTransaction()

Aborts the current transaction and rolls back any data modifications performed in the transaction.

retry

When an error is reported during the transaction, the error object caught by the catch contains an array with the attribute errorLabels. If the array contains the following two elements, it means that we can restart the corresponding transaction operation.

  • TransientTransactionError: in affairs open and the subsequent stages of data operation
  • UnknownTransactionCommitResult: in phase commit the transaction

The sample

After all this, are you eager to know how to write code to complete a transaction? Here is a simple example:

Scenario description: Suppose there are two tables in a trading system — Commodities, which records the name of the commodity, inventory quantity, and so on, and Orders, which records the order. When placing an order, users should first find the corresponding commodities in commodities table, judge whether the inventory quantity meets the demand of the order, subtract the corresponding value if so, and then insert an order data into orders table. In high concurrency scenarios, probably in the process of query inventory quantity and reduce inventory, and received a new order request, this time may be a problem, because when a new request in the query inventory, the previous operation has not yet completed reduce inventory operation, this time the query to the number of inventory may be sufficient, then began to perform the subsequent operations, In fact, it may be that the inventory has been reduced since the last operation, and a new order request may cause the number of orders actually created to exceed the inventory.

In the past, to solve this problem, we can use the way of “locking” commodity data, such as various locks based on Redis, allowing only one order to operate one commodity data at a time. This solution can solve the problem, but the disadvantage is that the code is more complex, and the performance will be lower. It can be much simpler if you use a database transaction:

Commodities Table data (stock) :

{ "_id" : ObjectId("5af0776263426f87dd69319a"), "name" : "Thanos Original Gloves."."stock" : 5 }
{ "_id" : ObjectId("5af0776263426f87dd693198"), "name" : "Thor's Hammer."."stock" : 2 }
Copy the code

Alter TABLE orders;

{ "_id" : ObjectId("5af07daa051d92f02462644c"), "commodity": ObjectId("5af0776263426f87dd69319a"), "amount": 2 }
{ "_id" : ObjectId("5af07daa051d92f02462644b"), "commodity": ObjectId("5af0776263426f87dd693198"), "amount": 3 }
Copy the code

Create an order with a single transaction (Mongo Shell) :

/ / execution txnFunc and in case of an TransientTransactionError retry
function runTransactionWithRetry(txnFunc, session) {
  while (true) {
    try {
      txnFunc(session); // Execute a transaction
      break;
    } catch (error) {
      if (
        error.hasOwnProperty('errorLabels') &&
        error.errorLabels.includes('TransientTransactionError')
      ) {
        print('TransientTransactionError, retrying transaction ... ');
        continue;
      } else {
        throwerror; }}}}/ / to commit the transaction and in case of an UnknownTransactionCommitResult try again
function commitWithRetry(session) {
  while (true) {
    try {
      session.commitTransaction();
      print('Transaction committed.');
      break;
    } catch (error) {
      if (
        error.hasOwnProperty('errorLabels') &&
        error.errorLabels.includes('UnknownTransactionCommitResult')
      ) {
        print('UnknownTransactionCommitResult, retrying commit operation ... ');
        continue;
      } else {
        print('Error during commit ... ');
        throwerror; }}}}// Create the order in a transaction
function createOrder(session) {
  var commoditiesCollection = session.getDatabase('mall').commodities;
  var ordersCollection = session.getDatabase('mall').orders;
  // Assume the number of items in the order
  var orderAmount = 3;
  // Assume the ID of the item
  var commodityID = ObjectId('5af0776263426f87dd69319a');

  session.startTransaction({
    readConcern: { level: 'snapshot' },
    writeConcern: { w: 'majority'}});try {
    var { stock } = commoditiesCollection.findOne({ _id: commodityID });
    if (stock < orderAmount) {
      print('Stock is not enough');
      session.abortTransaction();
      throw new Error('Stock is not enough');
    }
    commoditiesCollection.updateOne(
      { _id: commodityID },
      { $inc: { stock: -orderAmount } }
    );
    ordersCollection.insertOne({
      commodity: commodityID,
      amount: orderAmount,
    });
  } catch (error) {
    print('Caught exception during transaction, aborting.');
    session.abortTransaction();
    throw error;
  }

  commitWithRetry(session);
}

// Initiate a session
var session = db.getMongo().startSession({ readPreference: { mode: 'primary'}});try {
  runTransactionWithRetry(createOrder, session);
} catch (error) {
  // Error handling
} finally {
  session.endSession();
}
Copy the code

The above code looks a lot, but both runTransactionWithRetry and commitWithRetry functions can be pulled out as public functions without having to be written repeatedly for each operation. Since transactions are atomic operations, we don’t have to worry about data consistency caused by distributed concurrency. Isn’t that easier?

You may have noticed that the code sets two parameters when executing startTransaction — readConcern and writeConcern. This is the validation level for MongoDB read/write operations and is used to balance reliability and performance for data reads and writes in replicas. It would be too much to expand here, so interested friends suggest reading the official documentation:

ReadConcern:

Docs.mongodb.com/master/refe…

WriteConcern:

Docs.mongodb.com/master/refe…


The text/Tony

This article has been authorized by the author, the copyright belongs to chuangyu front. Welcome to indicate the source of this article. Link to this article: knownsec-fed.com/2018-08-24-…

To subscribe for more sharing from the front line of KnownsecFED development, please search our wechat official account KnownsecFED. Welcome to leave a comment to discuss, we will reply as far as possible.

Thank you for reading.