乐闻世界logo
搜索文章和话题

How does Mongoose handle transactions and concurrency control?

2月22日 20:08

Mongoose supports MongoDB's transaction functionality, allowing atomic operations across multiple documents or collections. Transactions are an important mechanism for ensuring data consistency.

Transaction Basics

Enable Transaction Support

MongoDB 4.0+ supports transactions on replica sets, and MongoDB 4.2+ supports transactions on sharded clusters.

javascript
const mongoose = require('mongoose'); // Connect to replica set mongoose.connect('mongodb://localhost:27017/mydb?replicaSet=myReplicaSet');

Create Session

javascript
const session = await mongoose.startSession();

Basic Transaction Operations

Simple Transaction

javascript
const session = await mongoose.startSession(); session.startTransaction(); try { // Execute operations const user = await User.create([{ name: 'John' }], { session }); const account = await Account.create([{ userId: user[0]._id, balance: 100 }], { session }); // Commit transaction await session.commitTransaction(); console.log('Transaction committed'); } catch (error) { // Abort transaction await session.abortTransaction(); console.log('Transaction aborted:', error); } finally { // End session session.endSession(); }

Update Operation Transaction

javascript
const session = await mongoose.startSession(); session.startTransaction(); try { // Transfer operation const fromAccount = await Account.findById(fromAccountId).session(session); const toAccount = await Account.findById(toAccountId).session(session); if (fromAccount.balance < amount) { throw new Error('Insufficient balance'); } fromAccount.balance -= amount; toAccount.balance += amount; await fromAccount.save({ session }); await toAccount.save({ session }); await session.commitTransaction(); } catch (error) { await session.abortTransaction(); throw error; } finally { session.endSession(); }

Transaction Options

Read and Write Concern

javascript
const session = await mongoose.startSession({ defaultTransactionOptions: { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' } }); session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });

Transaction Timeout

javascript
session.startTransaction({ maxTimeMS: 5000 // 5 second timeout });

Concurrency Control

Optimistic Locking

Use version numbers for optimistic locking:

javascript
const productSchema = new Schema({ name: String, stock: Number, version: { type: Number, default: 0 } }); productSchema.pre('save', function(next) { this.increment(); next(); }); // Check version when updating const product = await Product.findById(productId); product.stock -= quantity; try { await product.save(); } catch (error) { if (error.name === 'VersionError') { console.log('Document was modified by another process'); } }

Pessimistic Locking

Use findOneAndUpdate for pessimistic locking:

javascript
const session = await mongoose.startSession(); session.startTransaction(); try { // Find and lock document const product = await Product.findOneAndUpdate( { _id: productId, locked: false }, { locked: true }, { session, new: true } ); if (!product) { throw new Error('Product is locked or not found'); } // Execute operation product.stock -= quantity; await product.save({ session }); // Release lock product.locked = false; await product.save({ session }); await session.commitTransaction(); } catch (error) { await session.abortTransaction(); throw error; } finally { session.endSession(); }

Atomic Operations

Atomic Updates

javascript
// Atomic increment await User.findByIdAndUpdate(userId, { $inc: { balance: 100 } }); // Atomic conditional update await User.findOneAndUpdate( { _id: userId, balance: { $gte: 100 } }, { $inc: { balance: -100 } } ); // Atomic array operations await User.findByIdAndUpdate(userId, { $push: { tags: 'new-tag' }, $addToSet: { uniqueTags: 'unique-tag' } });

Batch Atomic Operations

javascript
// Batch update await User.updateMany( { status: 'active' }, { $set: { lastActive: new Date() } } ); // Batch delete await User.deleteMany({ status: 'deleted' });

Transaction Best Practices

  1. Keep transactions short: Longer transactions have higher conflict probability
  2. Avoid holding locks for long: Release resources as soon as possible
  3. Use appropriate isolation level: Choose based on business requirements
  4. Handle retry logic: Implement automatic retry mechanism
  5. Monitor transaction performance: Track transaction execution time and failure rate
  6. Design data model properly: Reduce need for cross-document transactions
javascript
// Auto-retry transaction async function runWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (error.name === 'TransientTransactionError' && i < maxRetries - 1) { console.log(`Retrying transaction (attempt ${i + 1})`); await new Promise(resolve => setTimeout(resolve, 100 * (i + 1))); } else { throw error; } } } }

Important Notes

  1. Transactions can only be used on replica sets or sharded clusters
  2. Transaction operations must be executed in the same session
  3. Transactions bring performance overhead, use carefully
  4. Avoid time-consuming operations in transactions
  5. Ensure proper handling of transaction errors and rollbacks
  6. Consider using atomic operations instead of simple transactions
标签:Mongoose