Node.js + MongoDB环境下预订系统高并发场景锁机制实现方案问询
Hey there! Since you already have optimistic concurrency control sorted for low contention, let's focus on pessimistic locking approaches that'll handle those 100 concurrent booking requests reliably. The key here is to prevent race conditions by ensuring only one request can modify the booking state at a time—critical when guarding a strict limit of 5 bookings.
Why Pessimistic Locking?
Optimistic concurrency works great when conflicts are rare, but with 100 requests fighting for 5 spots, you'd end up with lots of retries and frustrated users. Pessimistic locking locks the resource upfront, so requests queue up and only the first 5 succeed—no wasted retries.
Solution 1: Dedicated Lock Collection
This approach uses a separate collection to manage a global lock for your booking system. All requests must acquire this lock before checking booking counts or inserting new records.
Step-by-Step Implementation
Initialize the Lock Document
First, create abookingLockscollection and add a single lock document (run this once):const db = await getMongoDBConnection(); await db.collection('bookingLocks').updateOne( { _id: "booking_system_lock" }, { $setOnInsert: { isLocked: false } }, { upsert: true } );Acquire Lock, Validate, and Book
Use MongoDB's atomicfindOneAndUpdateto grab the lock—this ensures only one request can hold the lock at a time. Wrap the booking logic in atry/finallyblock to guarantee the lock is released even if something fails:async function createBooking(userId) { const db = await getMongoDBConnection(); let lockAcquired = false; try { // Acquire the lock atomically const lockResult = await db.collection('bookingLocks').findOneAndUpdate( { _id: "booking_system_lock", isLocked: false }, { $set: { isLocked: true, lockedAt: new Date() } }, { returnDocument: "after", upsert: true } ); if (!lockResult.value?.isLocked) { return { success: false, message: "System busy—please try again later." }; } lockAcquired = true; // Fetch limits and current bookings const userSettings = await db.collection('userSettings').findOne({}); const allowedBookings = userSettings?.userAllowedNumber || 5; const currentBookings = await db.collection('bookings').countDocuments({}); // Check if we can book if (currentBookings >= allowedBookings) { return { success: false, message: "Booking slots are full." }; } // Insert the new booking await db.collection('bookings').insertOne({ user_id: userId, bookedAt: new Date() }); return { success: true, message: "Booking confirmed!" }; } catch (err) { console.error("Booking error:", err); return { success: false, message: "Failed to process booking." }; } finally { // Release the lock no matter what if (lockAcquired) { await db.collection('bookingLocks').updateOne( { _id: "booking_system_lock" }, { $set: { isLocked: false } } ); } } }Add Lock Timeout (Optional)
To avoid deadlocks if a request crashes without releasing the lock, add a timeout check when acquiring the lock:const lockResult = await db.collection('bookingLocks').findOneAndUpdate( { _id: "booking_system_lock", $or: [ { isLocked: false }, { lockedAt: { $lt: new Date(Date.now() - 10000) } } // Release locks older than 10s ] }, { $set: { isLocked: true, lockedAt: new Date() } }, { returnDocument: "after", upsert: true } );
Solution 2: Atomic Counter Update (Simpler & More Efficient)
If your booking logic is straightforward (just count and limit), you can skip the dedicated lock collection and use an atomic counter in your userSettings document. This leverages MongoDB's atomic operations to eliminate race conditions entirely.
Step-by-Step Implementation
Update Your User Settings Document
Add acurrentBookedfield to track active bookings:await db.collection('userSettings').updateOne( { _id: "default_settings" }, { $setOnInsert: { userAllowedNumber: 5, currentBooked: 0 } }, { upsert: true } );Atomic Check & Book
UsefindOneAndUpdateto increment the counter only if it's below the limit. This operation is atomic—no two requests can increment the counter at the same time:async function createBookingWithCounter(userId) { const db = await getMongoDBConnection(); try { // Atomically check and increment the booking count const settingsUpdate = await db.collection('userSettings').findOneAndUpdate( { _id: "default_settings", currentBooked: { $lt: 5 } // Compare to userAllowedNumber if needed }, { $inc: { currentBooked: 1 } }, { returnDocument: "after", upsert: true } ); if (!settingsUpdate.value) { return { success: false, message: "Booking slots are full." }; } // Insert the booking record await db.collection('bookings').insertOne({ user_id: userId, bookedAt: new Date() }); return { success: true, message: "Booking confirmed!" }; } catch (err) { console.error("Booking error:", err); return { success: false, message: "Failed to process booking." }; } }
Which Solution Should You Choose?
- Dedicated Lock Collection: Use this if you need to run multiple dependent operations (e.g., check user-specific limits, validate other conditions) while holding the lock.
- Atomic Counter: Use this for simple limit checks—it's faster, uses fewer resources, and avoids the overhead of managing a separate lock.
Key Notes
- Keep lock hold time as short as possible—don't run slow operations (like external API calls) while holding the lock.
- For high traffic, consider adding a queue (like BullMQ) to manage booking requests, but the above solutions should handle 100 concurrent requests easily.
内容的提问来源于stack exchange,提问作者Naisarg Parmar

