Transactions

MongoDB 4.0 adds support for multi-document ACID transactions, making it the only open source database to combine the speed, flexibility, and power of the document model with ACID guarantees. Through snapshot isolation, transactions provide a consistent view of data, and enforce all-or-nothing execution to maintain data integrity.

try (ClientSession clientSession = client.startSession()) {
   clientSession.startTransaction();
   collection.insertOne(clientSession, docOne);
   collection.insertOne(clientSession, docTwo);
   clientSession.commitTransaction();
}

Transactions and Atomicity

Multi-document transactions are atomic:

  • When a transaction commits, all data changes made in the transaction are saved and visible outside the transaction. Until a transaction commits, the data changes made in the transaction are not visible outside the transaction.
  • When a transaction aborts, all data changes made in the transaction are discarded without ever becoming visible. For example, if any operation in the transaction fails, the transaction aborts and all data changes made in the transaction are discarded without ever becoming visible.

The following mongo shell example highlights the key components of using transactions:

The example omits retry logic and robust error handling for simplicity’s sake.

mongo "mongodb+srv://admatic-cluster-7qyyr.mongodb.net/test" --username admatic
MongoDB shell version v4.0.6
Enter password:
Implicit session: session { "id" : UUID("3d9e0104-8c52-4588-96d9-e61972704334") }
MongoDB server version: 4.0.6
MongoDB Enterprise Admatic-Cluster-shard-0:PRIMARY>

Create collections before Multi-document transactions

use hr
db.createCollection("employees")

use reporting
db.createCollection("events")
// Start a session.
session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );
session { "id" : UUID("63e3c526-32ed-4f47-90c9-4ec1c15b96c7") }
employeesCollection = session.getDatabase("hr").employees;
eventsCollection = session.getDatabase("reporting").events;

// Start a transaction
session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

// Operations inside the transaction
try {
   employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
   eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
} catch (error) {
   // Abort transaction on error
   session.abortTransaction();
   throw error;
}
{
  "acknowledged": true,
  "insertedId": ObjectId("5c776c7c9b6ff0d1ae47dd8d")
}
// Commit the transaction using write concern set at transaction start
session.commitTransaction();

session.endSession();

Transactions and Replica Sets

Starting in MongoDB 4.0, multi-document transactions are available for replica sets. Transactions for sharded clusters are scheduled for MongoDB 4.2

Transactions and Operations

For transactions:

  • You can specify read/write (CRUD) operations on existing collections. The collections can be in different databases.
  • You cannot read/write to collections in the config, admin, or local databases.
  • You cannot write to system.* collections.
  • You cannot return the supported operation’s query plan (i.e. explain).
  • For cursors created outside of transactions, you cannot call getMore inside a transaction.
  • For cursors created in a transaction, you cannot call getMore outside the transaction.

Transactions and Sessions

Transactions are associated with a session. That is, you start a transaction for a session. At any given time, you can have at most one open transaction for a session.

If a session ends and it has an open transaction, the transaction aborts.

Transactions in Applications

Retry Transaction

The individual write operations inside the transaction are not retryable, regardless of whether retryWrites is set to true.

If an operation encounters an error, the returned error may have an errorLabels array field. If the error is a transient error, the errorLabels array field contains TransientTransactionError as an element and the transaction as a whole can be retried.

For example, the following helper runs a function and retries the function if a TransientTransactionError is encountered:

void runTransactionWithRetry(Runnable transactional) {
    while (true) {
        try {
            transactional.run();
            break;
        } catch (MongoException e) {
            System.out.println("Transaction aborted. Caught exception during transaction.");

            if (e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)) {
                System.out.println("TransientTransactionError, aborting transaction and retrying ...");
                continue;
            } else {
                throw e;
            }
        }
    }
}

Retry Commit Operation

The commit operations are retryable write operations. If the commit operation encounters an error, MongoDB drivers retry the operation a single time regardless of whether retryWrites is set to true.

If the commit operation encounters an error, MongoDB returns an error with an errorLabels array field. If the error is a transient commit error, the errorLabels array field contains UnknownTransactionCommitResult as an element and the commit operation can be retried.

In addition to the single retry behavior provided by the MongoDB drivers, applications should take measures to handle UnknownTransactionCommitResult errors during transaction commits.

For example, the following helper commits a transaction and retries if a UnknownTransactionCommitResult is encountered:

void commitWithRetry(ClientSession clientSession) {
    while (true) {
        try {
            clientSession.commitTransaction();
            System.out.println("Transaction committed");
            break;
        } catch (MongoException e) {
            // can retry commit
            if (e.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
                System.out.println("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                System.out.println("Exception during commit ...");
                throw e;
            }
        }
    }
}

Retry Transaction and Commit Operation

Incorporating logic to retrying the transaction for transient errors and retrying the commit, the full code example is:

void runTransactionWithRetry(Runnable transactional) {
    while (true) {
        try {
            transactional.run();
            break;
        } catch (MongoException e) {
            System.out.println("Transaction aborted. Caught exception during transaction.");

            if (e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)) {
                System.out.println("TransientTransactionError, aborting transaction and retrying ...");
                continue;
            } else {
                throw e;
            }
        }
    }
}

void commitWithRetry(ClientSession clientSession) {
    while (true) {
        try {
            clientSession.commitTransaction();
            System.out.println("Transaction committed");
            break;
        } catch (MongoException e) {
            // can retry commit
            if (e.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
                System.out.println("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                System.out.println("Exception during commit ...");
                throw e;
            }
        }
    }
}

void updateEmployeeInfo() {
    MongoCollection<Document> employeesCollection = client.getDatabase("hr").getCollection("employees");
    MongoCollection<Document> eventsCollection = client.getDatabase("hr").getCollection("events");

    try (ClientSession clientSession = client.startSession()) {
        clientSession.startTransaction();

        employeesCollection.updateOne(clientSession,
                Filters.eq("employee", 3),
                Updates.set("status", "Inactive"));
        eventsCollection.insertOne(clientSession,
                new Document("employee", 3).append("status", new Document("new", "Inactive").append("old", "Active")));

        commitWithRetry(clientSession);
    }
}


void updateEmployeeInfoWithRetry() {
    runTransactionWithRetry(this::updateEmployeeInfo);
}

results matching ""

    No results matching ""