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.
javascriptconst mongoose = require('mongoose'); // Connect to replica set mongoose.connect('mongodb://localhost:27017/mydb?replicaSet=myReplicaSet');
Create Session
javascriptconst session = await mongoose.startSession();
Basic Transaction Operations
Simple Transaction
javascriptconst 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
javascriptconst 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
javascriptconst session = await mongoose.startSession({ defaultTransactionOptions: { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' } }); session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
Transaction Timeout
javascriptsession.startTransaction({ maxTimeMS: 5000 // 5 second timeout });
Concurrency Control
Optimistic Locking
Use version numbers for optimistic locking:
javascriptconst 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:
javascriptconst 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
- Keep transactions short: Longer transactions have higher conflict probability
- Avoid holding locks for long: Release resources as soon as possible
- Use appropriate isolation level: Choose based on business requirements
- Handle retry logic: Implement automatic retry mechanism
- Monitor transaction performance: Track transaction execution time and failure rate
- 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
- Transactions can only be used on replica sets or sharded clusters
- Transaction operations must be executed in the same session
- Transactions bring performance overhead, use carefully
- Avoid time-consuming operations in transactions
- Ensure proper handling of transaction errors and rollbacks
- Consider using atomic operations instead of simple transactions