TransientTransactionErrors, and users directly see error messages indicating operation failure.w:majority, and writes that had been confirmed as successful were lost after a primary/secondary switchover.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. |
// 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 }]});
withTransaction callback API to write transaction logic, which automatically handles transient error retries.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.TransientTransactionError.withTransaction callback API, which has a default overall retry time of approximately 120 seconds, increased the order success rate to 99.5%.withTransaction to implement a fund transfer transaction with automatic retries.from pymongo import MongoClientfrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernimport logginglogger = 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 Balanceaccount = 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.banktry: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
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();}}
// 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 latencyawait collection.updateOne({...}, {...}, { session });session.commitTransaction();// Correct: External calls are placed outside the transaction.const externalResult = await callExternalAPI(); // Complete the external call firstsession.startTransaction();await collection.updateOne({...}, {...}, { session });await collection.updateOne({...}, {...}, { session });session.commitTransaction();
// Optimistic Locking Implementationasync function updateWithOptimisticLock(collection, id, updateFn, maxRetries = 5) {for (let i = 0; i < maxRetries; i++) {// Read the current document and its version numberconst 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 numberconst 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 + jitterconst 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 deductionawait updateWithOptimisticLock(db.t_products,"P001",(product) => {if (product.stock < 1) throw new Error('Insufficient stock');return { stock: product.stock - 1 };});
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. |
majority when you need to read committed data, and use snapshot within transactions.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) |
local Read Concern.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".majority. The data read is guaranteed to be persisted and will not be lost due to a failover.// 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
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. |
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%. |
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}}
// 1. View the current active transactionsdb.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 serverStatusdb.serverStatus().transactions
피드백