Understanding Transactions in MongoDB

Diliru Munasingha
Ascentic Technology
5 min readJun 26, 2024
Photo by Rubaitul Azad on Unsplash

MongoDB supports transactions, enabling multiple operations to be executed in a single logical unit. This ensures data consistency and integrity, especially in complex applications requiring atomicity across multiple documents or collections.

What is a Transaction? A transaction in MongoDB allows you to execute a sequence of operations as a single, atomic unit. This means either all operations within the transaction succeed, or none do. If any operation fails, the entire transaction is rolled back, reverting the database to its previous state.

Key Concepts (ACID)

  • Atomicity: Ensures that all operations within a transaction are completed successfully. If any operation fails, all changes are undone.
  • Consistency: Guarantees that the database remains in a valid state before and after the transaction.
  • Isolation: Ensures that transactions are securely and independently processed without interference.
  • Durability: Once a transaction is committed, changes are permanent, even if the system crashes.

Using Transactions in MongoDB

To use transactions in MongoDB, you must be running a replica set or a sharded cluster with MongoDB 4.0 or later.

Basic Usage

  • Start a Session: Transactions are associated with client sessions.
const session = client.startSession();
  • Begin the Transaction:
session.startTransaction();
  • Perform Operations:
try {
// Operations within the transaction
db.collection('users').updateOne({ _id: 1 }, { $set: { name: 'Alice' } }, { session });

// Commit the transaction
await session.commitTransaction();
} catch (error) {
// If an error occurs, abort the transaction
await session.abortTransaction();
throw error; // Rethrow the error to handle it further up
} finally {
// End the session
session.endSession();
}

Single Collection Transactions vs. Multiple Collection Transactions

Single Collection Transactions

Scenario: Transactions involving operations within a single collection.

  • Use Case: Simple operations where updates, inserts or deletes affect only one collection.
  • Performance: Generally faster and less resource-intensive as there are fewer operations to track and manage.
  • Example:
const session = client.startSession();
session.startTransaction();
try {
db.collection('users').updateOne({ _id: 1 }, { $set: { name: 'Alice' } }, { session });
db.collection('users').updateOne({ _id: 2 }, { $set: { name: 'Bob' } }, { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}

Multiple Collection Transactions

Scenario: Transactions involving operations across multiple collections.

  • Use Case: Complex operations where consistency is required across different collections.
  • Performance: Can be slower and more resource-intensive due to the complexity of managing multiple operations across collections.
  • Example:
const session = client.startSession();
session.startTransaction();
try {
db.collection('users').updateOne({ _id: 1 }, { $set: { name: 'Alice' } }, { session });
db.collection('accounts').updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session });
db.collection('logs').insertOne({ event: 'Transfer', amount: 100, date: new Date() }, { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}

Locking Mechanism Behind Transactions

The locking mechanism in MongoDB ensures that transactions are isolated and consistent. Here’s how it works behind the scenes:

  1. Document-Level Locks: MongoDB employs a granular locking system that locks only the documents being modified. This minimizes contention and allows higher concurrency.
  2. WiredTiger Storage Engine: MongoDB uses the WiredTiger storage engine, which supports document-level locking. When a transaction modifies a document, WiredTiger acquires a lock on that document, preventing other operations from modifying it until the transaction is committed or aborted.
  3. Intent Locks: MongoDB uses intent locks at the collection and database levels. These are lightweight locks that indicate an intent to modify documents within a collection or database. This helps in coordinating more granular document-level locks.
  4. Two-Phase Locking: During a transaction, MongoDB follows a two-phase locking protocol, Growing Phase: The transaction acquires all the locks it needs as it executes its operations. Shrinking Phase: Once all the operations are complete, the transaction releases all the locks.
  5. Write Concern and Journaling: MongoDB ensures durability through write concern and journaling. When a transaction is committed, MongoDB writes the changes to the journal before acknowledging the commit, ensuring that changes are not lost even if there is a system crash.

Example Scenario: Document-Level Locking

Imagine an application transferring money between two bank accounts. This involves two operations: deducting from one account and crediting another. Document-level locking ensures that these operations can proceed without interference from other operations.

async function transferMoney(senderId, receiverId, amount) {
const session = client.startSession();
session.startTransaction();
try {
await db.collection('accounts').updateOne({ _id: senderId }, { $inc: { balance: -amount } }, { session });
await db.collection('accounts').updateOne({ _id: receiverId }, { $inc: { balance: amount } }, { session });
await session.commitTransaction();
console.log("Transaction committed.");
} catch (error) {
await session.abortTransaction();
console.error("Transaction aborted due to error: ", error);
} finally {
session.endSession();
}
}

In this scenario, MongoDB locks the documents for the sender’s and receiver’s accounts during the transaction, ensuring no other operation can modify these documents until the transaction completes.

Issues with Transactions Under Higher Loads

Implementing transactions in MongoDB under higher loads can introduce several challenges:

  • Increased Latency: Transactions can add latency because they require acquiring and releasing locks, ensuring data consistency, and potentially coordinating across multiple nodes in a replica set or sharded cluster.
  • Contention and Deadlocks: High concurrency can lead to contention for locks, slowing down transactions. Deadlocks may also occur when two or more transactions are waiting for each other to release locks.
  • Resource Consumption: Transactions consume more resources, such as memory and CPU, especially when dealing with large numbers of operations or high concurrency.
  • Rollback Overheads: If a transaction fails and requires a rollback, the system needs to revert all changes, which can be resource-intensive and slow under high load.

Mitigation Strategies for High Loads

  • Optimize Transactions: Keep transactions short and limit the number of operations within a transaction to reduce lock duration and resource consumption.
  • Use Appropriate Write Concern: Choose a write concern that balances durability with performance. For less critical data, a lower write concern can improve performance.
  • Monitor and Tune Performance: Regularly monitor transaction performance and tune database configurations. Adjust parameters like maxTransactionLockRequestTimeoutMillis to prevent long wait times for locks.
  • Scale Out: Use sharding to distribute the load across multiple nodes. Ensure that transactions are designed to minimize cross-shard operations, which can be more complex and slower.
  • Handle Deadlocks Gracefully: Implement retry logic in your application to handle deadlocks. When a deadlock is detected, abort the transaction and retry after a brief wait.
  • Resource Allocation: Ensure your hardware resources (CPU, memory, I/O) are adequate for the expected load. Use provisioning and scaling strategies to handle peak loads.

Isolation Levels

MongoDB supports multiple isolation levels for transactions:

  • Read Committed: Ensures that each read operation within a transaction sees only data that has been committed at the time the read operation starts.
  • Repeatable Read: Guarantees that all reads within a transaction see the same data set. Changes made by other transactions after the transaction starts are not visible.

These isolation levels allow developers to choose the appropriate level of consistency versus performance for their applications.

Conclusion

Transactions in MongoDB provide robust mechanisms for maintaining data integrity and consistency, especially in multi-document and multi-collection operations. The locking mechanism ensures isolation and minimizes contention, allowing developers to handle complex operations reliably. However, under higher loads, transactions can introduce challenges such as increased latency, contention, and resource consumption. By employing optimization strategies, specific error handling, leveraging advanced features like isolation levels, developers can effectively manage and mitigate these issues. This ensures that MongoDB transactions remain efficient and reliable, meeting the demands of modern, data-intensive applications.

Have I missed anything? Let me know in the comments!

--

--