The isolation levels and concurrency control of transactions
Transaction Isolation Levels and Concurrency Control
In database systems, transaction isolation levels and concurrency control are key mechanisms for ensuring data consistency and integrity. MongoDB, as a representative NoSQL database, began supporting multi-document transactions after version 4.0. Its implementation shares similarities with traditional relational databases but also has unique designs.
Basics of Transaction Isolation Levels
MongoDB supports the following four standard isolation levels:
- Read Uncommitted: Allows reading uncommitted data changes, which may lead to dirty reads.
- Read Committed: Only allows reading committed data, avoiding dirty reads but potentially causing non-repeatable reads.
- Repeatable Read: Ensures that multiple reads of the same data within the same transaction yield consistent results.
- Serializable: The highest isolation level, ensuring complete serial execution of transactions.
MongoDB defaults to the Read Committed isolation level, consistent with most relational databases. However, in sharded clusters, the default isolation level is downgraded to Read Uncommitted.
// Basic MongoDB transaction usage example
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
const users = session.getDatabase("test").users;
const orders = session.getDatabase("test").orders;
users.insertOne({ _id: 1, name: "Alice", balance: 100 });
orders.insertOne({ user_id: 1, amount: 50 });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
MongoDB's Concurrency Control Mechanism
MongoDB employs a Multi-Version Concurrency Control (MVCC) mechanism to achieve transaction isolation. Each document has version information, and write operations create new versions rather than directly modifying existing data. This design offers several key features:
- Read operations do not block write operations: Reads are based on a snapshot of data at a specific point in time.
- Write operations do not block read operations: Writing new versions does not affect transactions reading old versions.
- Optimistic concurrency control: Conflict detection occurs during the commit phase rather than the execution phase.
In sharded clusters, MongoDB uses a Two-Phase Commit (2PC) protocol to ensure atomicity for cross-shard transactions. This results in higher latency but guarantees data consistency.
Practical Impact of Isolation Levels
Different isolation levels affect applications in the following ways:
Typical scenario for Read Committed:
// Transaction 1
const session1 = db.getMongo().startSession();
session1.startTransaction({ readConcern: { level: "local" } });
// Transaction 2
const session2 = db.getMongo().startSession();
session2.startTransaction({ readConcern: { level: "local" } });
// Transaction 1 reads data
const user1 = session1.getDatabase("test").users.findOne({ _id: 1 });
// Transaction 2 updates the same data
session2.getDatabase("test").users.updateOne(
{ _id: 1 },
{ $inc: { balance: 10 } }
);
session2.commitTransaction();
// Transaction 1 reads again - may see different results under Read Committed
const user2 = session1.getDatabase("test").users.findOne({ _id: 1 });
Implementation of Repeatable Read: MongoDB achieves Repeatable Read through snapshot isolation. A snapshot of the data is taken when the transaction starts, and subsequent reads are based on this snapshot:
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
// First read
const user1 = session.getDatabase("test").users.findOne({ _id: 1 });
// Another transaction modifies the data
db.users.updateOne({ _id: 1 }, { $set: { name: "Bob" } });
// Second read - still sees the data as it was at the start of the transaction
const user2 = session.getDatabase("test").users.findOne({ _id: 1 });
Write Conflicts and Retry Mechanisms
MongoDB uses optimistic concurrency control, where write conflicts are typically detected only during commit. Common conflict handling patterns:
async function transferFunds(senderId, receiverId, amount) {
let retries = 3;
while (retries-- > 0) {
const session = db.getMongo().startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
const users = session.getDatabase("test").users;
const sender = await users.findOne({ _id: senderId });
if (sender.balance < amount) {
throw new Error("Insufficient balance");
}
await users.updateOne(
{ _id: senderId },
{ $inc: { balance: -amount } }
);
await users.updateOne(
{ _id: receiverId },
{ $inc: { balance: amount } }
);
await session.commitTransaction();
return;
} catch (error) {
if (error.hasErrorLabel("TransientTransactionError") && retries > 0) {
await session.abortTransaction();
continue;
}
throw error;
} finally {
session.endSession();
}
}
}
Performance Considerations and Best Practices
MongoDB transaction performance is significantly affected by the following factors:
- Transaction duration: Long-running transactions increase the likelihood of conflicts and memory pressure.
- Document size: Large documents occupy more version storage space.
- Number of operations: Too many operations in a single transaction can degrade performance.
Optimization recommendations:
- Keep transactions short (typically <1000ms).
- Limit the number of operations in a single transaction.
- Avoid handling large documents within transactions.
- Set appropriate writeConcern and readConcern levels.
// Optimized transaction example
async function processOrder(userId, items) {
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: 1 }, // Lower requirements to improve performance
maxTimeMS: 5000 // Set timeout
});
try {
// Query only necessary fields
const user = await session.getDatabase("test").users.findOne(
{ _id: userId },
{ projection: { balance: 1 } }
);
// Batch insert order items
await session.getDatabase("test").orders.insertMany(
items.map(item => ({
user_id: userId,
product_id: item.id,
quantity: item.qty,
date: new Date()
}))
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
Handling Special Scenarios
Cross-Shard Transactions: Transactions in MongoDB sharded clusters require special attention:
// Sharded transactions require stricter configuration
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary" // Must read from the primary node
});
try {
// Operations may involve collections on different shards
await session.getDatabase("test").users.updateOne(
{ _id: userId },
{ $inc: { balance: -amount } }
);
await session.getDatabase("inventory").products.updateOne(
{ _id: productId },
{ $inc: { stock: -1 } }
);
await session.commitTransaction();
} catch (error) {
// Sharded transactions have higher failure rates and require robust error handling
if (error.hasErrorLabel("UnknownTransactionCommitResult")) {
// Manual intervention may be needed
}
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
Change Streams and Transactions: MongoDB change streams can be combined with transactions to implement reliable event-driven architectures:
// Publish events within a transaction
async function createUserWithEvent(userData) {
const session = db.getMongo().startSession();
session.startTransaction();
try {
const user = await session.getDatabase("test").users.insertOne(userData);
await session.getDatabase("events").notifications.insertOne({
type: "USER_CREATED",
user_id: user.insertedId,
timestamp: new Date()
});
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
// Change stream consumers can ensure processing of complete transactions
const changeStream = db.notifications.watch();
changeStream.on("change", (change) => {
// Notifications received here are all from committed transactions
});
Monitoring and Troubleshooting
MongoDB provides multiple ways to monitor transaction performance:
- currentOp command: View running transactions.
db.adminCommand({
currentOp: true,
$or: [
{ op: "command", "command.abortTransaction": { $exists: true } },
{ op: "command", "command.commitTransaction": { $exists: true } },
{ op: "command", "command.startTransaction": { $exists: true } }
]
})
- serverStatus output:
db.serverStatus().transactions
- Profiler:
db.setProfilingLevel(1, { slowms: 50 })
Common issue diagnosis patterns:
- High abort rate: Usually indicates severe concurrency conflicts, requiring business logic refactoring.
- Long transactions: Check if transactions contain unnecessary operations or lack timeouts.
- Lock waits: Optimize indexes to reduce scan ranges.
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn