tencent cloud

TencentDB for MongoDB

Transaction and Data Consistency

Download
フォーカスモード
フォントサイズ
最終更新日: 2026-07-10 21:55:25

Scenarios

MongoDB has supported multi-document transactions since version 4.0, providing a solution for scenarios that require atomicity across documents. In actual production environments, however, improper use of transactions often leads to severe availability issues, such as:
Excessive use of transactions leads to a sharp performance drop: Wrapping single-document operations with unnecessary transactions causes the WiredTiger Cache to be filled with snapshot data, resulting in a 40% to 50% decrease in system throughput.
Long transactions cause deadlocks and timeouts: When a transaction contains time-consuming operations such as external API calls, lock resources are held for extended periods, causing subsequent transactions to queue up and wait, or even time out and roll back.
A sharp increase in high-concurrency transaction conflicts: The lack of a conflict resolution mechanism causes write conflicts to generate a large number of TransientTransactionErrors, and users directly see error messages indicating operation failure.
Improper Write Concern configuration leads to data loss: Core services did not use w:majority, and writes that had been confirmed as successful were lost after a primary/secondary switchover.
This document aims to provide standard specifications for transaction usage and data consistency configuration, helping developers use transactions correctly and avoid performance issues caused by over-reliance on transactions.

Transaction Usage Decisions

Guideline 1: Prioritize Single-Document Atomicity

Core Action: Place data that requires atomic operations within the same document through proper document model design, rather than relying on multi-document transactions.
MongoDB provides inherent atomicity for all operations on a single document, including multi-field updates, embedded document modifications, and array operations, without requiring explicit transactions. By designing with embedded documents, operations that would otherwise require cross-collection transactions to ensure consistency can be transformed into atomic operations on a single document, thereby avoiding the performance overhead of transactions.
Scenario
Recommended Solution
Description
Update multiple fields in a single document
No transaction required
Single-document operations are inherently atomic.
Order + Order Items
No transaction required
Embedded design, single-document operation.
Fund transfer (Debit from A + Credit to B)
Transaction required
Cross-document atomicity is required.
Order processing + Inventory deduction
Transaction required
Cross-collection correlated operations.
Batch insertion of independent documents
No transaction required
Documents are independent, and atomicity is not required.
Business Application: An e-commerce system stores orders and order items in two separate collections (relational thinking), requiring transactions to ensure consistency between the two collections when an order is created.
Problem Manifestation: During peak hours, thousands of orders per second have transactions enabled simultaneously. This causes the WiredTiger Cache to be filled with snapshot data, triggering an eviction storm and resulting in an 80% drop in system throughput.
Optimization Action: The system was redesigned using embedded documents (with order items embedded within the order). Operations on a single document are inherently atomic, eliminating the need for transactions, and performance returned to normal.
Single-Document Atomicity Example: The following example demonstrates how to avoid using multi-document transactions through embedded document design.
// Flawed Design: Orders and order items are stored in two separate collections, requiring transactions to ensure consistency.
const session = client.startSession();
session.startTransaction();
try {
await db.t_orders.insertOne(
{ orderId: "ORD001", userId: "U001", total: 299 },
{ session }
);
await db.t_order_items.insertMany([
{ orderId: "ORD001", productId: "P001", quantity: 1, price: 199 },
{ orderId: "ORD001", productId: "P002", quantity: 1, price: 100 }
], { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}

// Correct Design: Using embedded documents, operations on a single document are inherently atomic and do not require transactions.
await db.t_orders.insertOne({
orderId: "ORD001",
userId: "U001",
total: 299,
items: [
{ productId: "P001", quantity: 1, price: 199 },
{ productId: "P002", quantity: 1, price: 100 }
]
});

Guideline 2: Use Transactions Only When Necessary

Core Action: Use transactions only when operations span multiple documents or collections and atomicity must be guaranteed. Do not wrap single-document operations in transactions.
The performance overhead of transactions stems from three aspects: snapshot maintenance occupying the WiredTiger Cache, lock waits increasing latency, and transaction coordination consuming CPU. Unnecessary transaction wrapping reduces overall system throughput and increases the risk of deadlocks.
Business Application: A system developer, for the sake of "safety", wraps all database operations in transactions, even using transactions for single-document updates.
Problem Manifestation: Overall system latency increased by 50%, concurrent processing capacity decreased by 40%, and transaction timeout errors occurred frequently during peak hours.
Optimization Action: Unnecessary transaction wrapping was removed, with transactions retained only for cross-document operations, and performance returned to normal.

Transaction Writing Guidelines

Guideline 3: Use the Officially Recommended Transaction Callback API

Core Action: Use the withTransaction callback API to write transaction logic, which automatically handles transient error retries.
In high-concurrency scenarios, MongoDB transactions may trigger a TransientTransactionError due to write conflicts. If the application layer does not implement retry logic, users will directly see error messages indicating operation failure. The withTransaction callback API has a built-in mechanism for automatically retrying transient errors, enabling recovery without user awareness.
Business Application: In an inventory deduction scenario, multiple requests simultaneously compete for the same product inventory, frequently triggering write conflicts that result in TransientTransactionError.
Problem Manifestation: The application layer lacks retry logic, users see "order failed" error messages, and the order success rate is only 85%.
Optimization Action: Using the withTransaction callback API, which has a default overall retry time of approximately 120 seconds, increased the order success rate to 99.5%.
Python Transaction Callback Example: The following example demonstrates how to use withTransaction to implement a fund transfer transaction with automatic retries.
from pymongo import MongoClient
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern
import logging

logger = logging.getLogger(__name__)


def transfer_funds(db, session, from_account, to_account, amount):
"Fund Transfer Business Logic"
accounts = db.accounts

# Deduct Funds (CAS Balance Check)
result = accounts.update_one(
{"_id": from_account, "balance": {"$gte": amount}},
{"$inc": {"balance": -amount}},
session=session
)
if result.matched_count != 1:
# Distinguish Between Non-existent Accounts and Insufficient Balance
account = accounts.find_one({"_id": from_account}, session=session)
if not account:
raise Exception(f"Deduction account does not exist: {from_account}")
raise Exception(
f"Insufficient balance: {from_account}, current balance {account['balance']}, required deduction {amount}"
)

# Credit Funds (Must Verify Target Account Exists)
result = accounts.update_one(
{"_id": to_account},
{"$inc": {"balance": amount}},
session=session
)
if result.matched_count != 1:
raise Exception(f"Credit account does not exist: {to_account}")


# Use the with_transaction Callback API (Automatically Retries TransientTransactionError)
client = MongoClient("mongodb://...")
db = client.bank

try:
with client.start_session() as session:
session.with_transaction(
lambda s: transfer_funds(db, s, "A001", "A002", 100),
read_concern=ReadConcern(level="snapshot"),
write_concern=WriteConcern(w="majority", j=True)
)
logger.info("Transfer successful: A001 -> A002, amount 100")
except Exception as e:
logger.error(f"Transfer failed: {e}")
raise
Node.js Transaction Callback Example: The following example demonstrates the usage of transaction callbacks in the Node.js driver.
const { MongoClient } = require('mongodb');

async function transferFunds(client, fromAccount, toAccount, amount) {
const session = client.startSession();

try {
// Use the withTransaction Callback API (Automatically Retries TransientTransactionError)
await session.withTransaction(async () => {
const accounts = client.db('bank').collection('accounts');

// Deduct Funds (CAS Balance Check)
const debitResult = await accounts.updateOne(
{ _id: fromAccount, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);

if (debitResult.matchedCount !== 1) {
// Distinguish Between "Account Does Not Exist" and "Insufficient Balance"
const account = await accounts.findOne(
{ _id: fromAccount },
{ session }
);
if (!account) {
throw new Error(`Deduction account does not exist: ${fromAccount}`);
}
throw new Error(
`Insufficient balance: ${fromAccount}, current balance ${account.balance}, required deduction ${amount}`
);
}

// Credit Funds (Must Verify Target Account Exists, Otherwise Funds Will Be Lost)
const creditResult = await accounts.updateOne(
{ _id: toAccount },
{ $inc: { balance: amount } },
{ session }
);

if (creditResult.matchedCount !== 1) {
throw new Error(`Credit account does not exist: ${toAccount}`);
}

}, {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority', j: true }
});

console.log(`Transfer successful: ${fromAccount} -> ${toAccount}, amount ${amount}`);
} finally {
await session.endSession();
}
}

Guideline 4: Keep Transaction Execution Time Within Seconds

Core Action: Transaction execution time must be controlled within seconds. Do not include time-consuming operations such as external API calls or file I/O within transactions.
Long-running transactions continuously hold lock and snapshot resources. The longer a transaction runs, the higher the probability of conflict with other transactions. Additionally, the occupied WiredTiger Cache snapshot cannot be released, causing subsequent transactions to queue up and wait. The default transaction timeout in MongoDB is 60 seconds, after which a transaction is automatically rolled back.
Business Application: A payment system calls an external risk control API within a transaction to verify transaction legitimacy. Network latency causes the transaction to persist for several seconds.
Symptom: Long-running transactions hold lock and snapshot resources, causing other transactions to queue up and wait, which leads to a sharp drop in system throughput.
Optimization Action: The risk control call was moved outside the transaction. Transaction time decreased from 3 seconds to 100 ms, and system stability was restored.
Transaction Simplification Example: The following example demonstrates how to move time-consuming operations out of a transaction to shorten transaction execution time.
// Error: A time-consuming external call is included within the transaction.
session.startTransaction();
await collection.updateOne({...}, {...}, { session });
const result = await callExternalAPI(); // External call with uncontrollable latency
await collection.updateOne({...}, {...}, { session });
session.commitTransaction();
// Correct: External calls are placed outside the transaction.
const externalResult = await callExternalAPI(); // Complete the external call first
session.startTransaction();
await collection.updateOne({...}, {...}, { session });
await collection.updateOne({...}, {...}, { session });
session.commitTransaction();

Transaction Alternatives

Guideline 5: Use Optimistic Locking Instead of Transactions

Core Action: For "read → modify → write" scenarios, you can use version numbers to implement optimistic locking, replacing multi-document transactions.
The core idea of optimistic locking is to record the document version number during a read operation and use the version number as a query condition during an update. If the version number has changed, it indicates that the data has been modified by another request. In this case, the update fails and should be retried. Compared to transactions, optimistic locking does not occupy snapshot or lock resources, resulting in lower database pressure and higher throughput.
Business Application: In an inventory deduction scenario, transactions are used to guarantee atomicity, but the conflict rate is high.
Symptom: Under high concurrency, transactions frequently conflict, retry costs are high, and database snapshot pressure is significant.
Optimization Action: Optimistic locking is adopted. Transaction conflicts are shifted to application-layer retries, and database pressure is significantly reduced.
Optimistic Locking Implementation Example: The following example demonstrates the method for implementing optimistic locking based on version numbers.
// Optimistic Locking Implementation
async function updateWithOptimisticLock(collection, id, updateFn, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
// Read the current document and its version number
const doc = await collection.findOne({ _id: id });
if (!doc) {
throw new Error(`Document does not exist: ${id}`);
}
const currentVersion = doc.version || 0;

// Apply business logic (Business exceptions are thrown directly without retry)
const updates = updateFn(doc);

// Conditional update with version number
const result = await collection.updateOne(
{ _id: id, version: currentVersion },
{
$set: updates,
$inc: { version: 1 }
}
);

// Use matchedCount to determine whether CAS is hit, not modifiedCount.
if (result.matchedCount === 1) {
return true; // Update successful
}

// Version conflict, retry after exponential backoff + jitter
const backoff = Math.random() * Math.pow(2, i) * 10;
await new Promise(resolve => setTimeout(resolve, backoff));
}
throw new Error(`Optimistic locking update failed, maximum retry count exceeded: ${maxRetries}`);
}

// Usage example: Inventory deduction
await updateWithOptimisticLock(
db.t_products,
"P001",
(product) => {
if (product.stock < 1) throw new Error('Insufficient stock');
return { stock: product.stock - 1 };
}
);

Guideline 6: Use an Eventual Consistency Scheme for Non-Critical Scenarios

Core Action: For non-critical scenarios, you can use message queues + compensation mechanisms to achieve eventual consistency, avoiding the performance overhead of strong consistency transactions.
Strong consistency transactions require all operations to either succeed entirely or roll back entirely. When a transaction contains multiple non-critical operations (such as sending notifications, logging, or updating statistics), failure in any single step causes the entire transaction to roll back, reducing the success rate of the core operation. The eventual consistency solution decouples core operations from non-critical ones. After the core operation succeeds, other operations are processed asynchronously, with retries or compensation applied upon failure.
Scenario
Recommended Solution
Description
Fund transfer (fund-related)
Multi-document transaction
Strong consistency required
Order + Notification
Eventual consistency
Notification failure does not affect the order.
Order + Points
Eventual consistency
Points can be compensated asynchronously.
Order + Inventory
Transaction or optimistic lock
Inventory must be accurate.
Business Application: After an order is created, operations such as sending notifications, logging, and updating statistics are required.
Problem Manifestation: If strong consistency is enforced using transactions, a failure in any single step causes the order creation to fail, resulting in an order creation success rate of only 95%.
Optimization Action: Switch to an eventual consistency solution. After an order is successfully created, send a message, and have consumers asynchronously process other operations, with retries or compensation applied upon failure. The order creation success rate is increased to 99.9%.

Read Concern Configuration

Guideline 7: Select Read Concern Based on the Scenario

Core action: Use majority when you need to read committed data, and use snapshot within transactions.
MongoDB's Read Concern determines the consistency level for data reads. With the default local, data that has not been replicated to a majority of nodes may be read, and this data can be lost after a primary/secondary switchover. majority ensures that the data read has been acknowledged by a majority of nodes and will not be lost due to a failover.
Read Concern
Description
Use Cases
`local`
Reads the latest local data (default).
General reads
`majority`
Reads data that has been acknowledged by a majority of nodes.
Read committed data is required.
`snapshot`
Snapshot-isolated reads within a transaction
Multi-document transaction
`linearizable`
Linearizable reads
Strong consistency required (lower performance)
Business Application: A system immediately reads and verifies data after a write, using the default local Read Concern.
Problem Manifestation: local may read data that has not been replicated to a majority of nodes. This data is lost after a primary/secondary switchover, causing confusion about "successful writes but missing data".
Optimization Action: Switch to majority. The data read is guaranteed to be persisted and will not be lost due to a failover.
Read Concern Configuration Example: The following example demonstrates how to configure different Read Concern levels.
// Default: local (may read data that is not persisted)
db.t_orders.find({ orderId: "ORD001" })
// Recommended: majority (reads persisted data)
db.t_orders.find({ orderId: "ORD001" }).readConcern("majority")
// Within a transaction: snapshot (snapshot isolation)
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
// Connection string configuration method (globally effective)
// mongodb://user:pwd@host1,host2,host3/db?readConcernLevel=majority

Transaction Checklist

Pre-Launch Checklist

Check Item
Verification Method
Passing Criteria
Whether a transaction is truly required
Analyze business logic
Transactions are not required for operations that can be resolved with a single document.
Transaction duration sufficiently short or not
Monitor transaction execution time
< 1 second
External calls or not
Code review
No network calls within the transaction.
Callback API used or not
Code review
Use `withTransaction`.
Retry configured or not
Code review
Handle `TransientTransactionError`.
Read Concern appropriateness or not
Check configuration.
Use `snapshot` within transactions.
Write Concern appropriateness or not
Check configuration.
Use `majority` for core business.

Regular Inspection

Check Item
Verification Method
Action Recommendation
Transaction execution time
Monitoring Dashboard
Investigate transactions with a P99 latency exceeding 1 second.
Transaction conflict rate
Monitor the frequency of `TransientTransactionError`.
Optimize when the conflict rate > 5%.
WiredTiger Cache utilization
`serverStatus().wiredTiger.cache`
Monitor when utilization > 80%.

FAQs

Q1: When Must Transactions Be Used?

Answer: Use multi-document transactions only in the following scenarios:
1. Cross-document financial operations (such as debiting and crediting in a transfer).
2. Cross-collection correlated operations (such as creating an order while deducting inventory).
3. Batch operations that require atomic reads and writes across multiple documents.
Operations such as multi-field updates within a single document, embedded document modifications, and array operations do not require transactions.

Q2: How to Handle TransientTransactionError in Transactions

Answer: Use the withTransaction callback API, or monitor the TransientTransactionError error code and automatically retry. If manual handling is still required:
while (retries < maxRetries) {
try {
session.startTransaction();
// ... Business Logic ...
session.commitTransaction();
break; // Exit successfully
} catch (error) {
session.abortTransaction();
if (error.hasErrorLabel("TransientTransactionError") && retries < maxRetries) {
retries++;
continue; // Retry
}
throw error; // Non-transient error, throw it
}
}

Q3: How to Monitor Transaction Health

// 1. View the current active transactions
db.currentOp({ "transaction": { $exists: true } })

// 2. Check the transaction execution time
// Focus on the transaction.timeActiveMicros field
// Investigate the cause for transactions that exceed 1 second.

// 3. Monitor transaction conflicts
// Focus on the transactions metric in serverStatus
db.serverStatus().transactions

ヘルプとサポート

この記事はお役に立ちましたか?

フィードバック