Oplog & ACID Transactions
Oplog & ACID Transactions
While MongoDB has always guaranteed atomicity at the single-document level, modern applications often require cross-collection or multi-document consistency. This is achieved through Multi-Document ACID Transactions.
🏗️ 1. The Oplog: Foundation of Consistency
The Oplog (Operation Log) is a capped collection in the local database that stores all write operations applied to a Replica Set’s Primary.
- Idempotency: Every operation in the Oplog is idempotent, meaning applying it multiple times results in the same state.
- Tailing the Oplog: Secondary nodes continuously poll the Primary’s Oplog to replicate changes.
- Retention: Because it is a capped collection, the Oplog has a fixed size. If it overflows, Secondaries that have fallen too far behind will need a full resync.
🚀 2. Multi-Document ACID Transactions
Since version 4.0, MongoDB supports full ACID (Atomicity, Consistency, Isolation, Durability) transactions across multiple collections.
Core Features
- All-or-Nothing: All operations within a transaction either succeed and are committed together, or all are aborted.
- Isolation: Operations in a transaction are not visible to other clients until they are committed (Snapshot Isolation).
Technical Requirements
- Replica Set: Transactions require a Replica Set (or a Sharded Cluster).
- Driver Support: You must use a modern driver (e.g., Pymongo 3.7+).
⚡ 3. Implementation Examples
Pymongo Example: Banking Transaction
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017/?replicaSet=rs0')
db = client['bank']
def transfer_funds(session, from_id, to_id, amount):
accounts = db['accounts']
# Step 1: Deduct from A
accounts.update_one(
{"_id": from_id, "balance": {"$gte": amount}},
{"$inc": {"balance": -amount}},
session=session
)
# Step 2: Add to B
accounts.update_one(
{"_id": to_id},
{"$inc": {"balance": amount}},
session=session
)
with client.start_session() as session:
# Use with_transaction to automatically handle retries
session.with_transaction(
lambda s: transfer_funds(s, "account_A", "account_B", 500)
)Mongo Shell Example: Manual Transaction
const session = db.getMongo().startSession();
session.startTransaction();
try {
db.inventory.updateOne(
{ _id: "sku_123" },
{ $inc: { quantity: -1 } },
{ session }
);
db.orders.insertOne(
{ order_id: "order_abc", item: "sku_123" },
{ session }
);
session.commitTransaction();
} catch (error) {
session.abortTransaction();
} finally {
session.endSession();
}🛡️ 4. Performance and Constraints
- Locking: Transactions hold locks on modified documents until they are committed or aborted, which can impact performance.
- Time Limits: By default, transactions must complete within 60 seconds (
transactionLifetimeLimitSeconds). - Execution Cost: Transactions incur additional overhead in the WiredTiger engine to maintain multiple snapshots of the data.
💡 Best Practices
- Design for Single-Document Atomicity: Always prefer Embedded Documents over transactions when possible.
- Keep Transactions Short: Avoid long-running logic or external API calls inside a transaction block.
- Handle Transient Errors: Some transaction failures (like “Write Conflict”) are transient and should be retried automatically (as shown in the Pymongo example).