You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js + MongoDB环境下预订系统高并发场景锁机制实现方案问询

Pessimistic Locking Solutions for Your Node.js + MongoDB Booking System

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

  1. Initialize the Lock Document
    First, create a bookingLocks collection 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 }
    );
    
  2. Acquire Lock, Validate, and Book
    Use MongoDB's atomic findOneAndUpdate to grab the lock—this ensures only one request can hold the lock at a time. Wrap the booking logic in a try/finally block 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 } }
          );
        }
      }
    }
    
  3. 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

  1. Update Your User Settings Document
    Add a currentBooked field to track active bookings:

    await db.collection('userSettings').updateOne(
      { _id: "default_settings" },
      { $setOnInsert: { userAllowedNumber: 5, currentBooked: 0 } },
      { upsert: true }
    );
    
  2. Atomic Check & Book
    Use findOneAndUpdate to 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 14:18:15