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

如何解决Socket.io中创建/加入单房间限2人时的并发请求问题

解决并发请求下房间超员的问题

你碰到的这个问题是典型的竞态条件导致的——多个用户同时发起请求时,它们会同时读取到同一个有空位的房间状态,接着都执行加入操作,最终突破了2人的房间限制。因为内存里的rooms对象没有任何并发控制,查找房间和修改房间状态这两步不是原子操作,中间的空隙就会被并发请求钻空子。

问题分析

看你的代码逻辑:

  1. 先遍历rooms查找有空位的房间
  2. 如果找到就加入,没找到就创建新房间
    但这两步之间没有任何同步机制,当两个请求同时完成第一步的查找,都发现某个房间还有1个空位,接着就会同时执行push用户的操作,最终房间里就有3个人了。

解决方案:给房间操作加互斥锁

因为你用的是内存存储,我们可以手动实现一个简单的互斥锁,确保同一时间只有一个请求能执行房间的查找、创建/修改操作,彻底消除竞态条件。

首先实现一个基于Promise的锁工具:

// 用于存储房间相关的锁,这里我们用一个全局锁保护整个rooms对象的操作
const roomLocks = new Map();

// 获取锁
function acquireLock(lockKey) {
  let lockPromise = roomLocks.get(lockKey);
  if (!lockPromise) {
    let resolveLock;
    lockPromise = new Promise(resolve => {
      resolveLock = resolve;
    });
    lockPromise.resolve = resolveLock;
    roomLocks.set(lockKey, lockPromise);
  }
  return lockPromise;
}

// 释放锁
function releaseLock(lockKey) {
  const lockPromise = roomLocks.get(lockKey);
  if (lockPromise) {
    lockPromise.resolve();
    roomLocks.delete(lockKey);
  }
}

然后修改你的事件处理函数,用锁包裹核心逻辑:

const rooms = {};
const roomLocks = new Map();

// 上面的acquireLock和releaseLock函数放在这里

module.exports = function ({ io, socket }) {
 socket.on("/createJoinPublicRequest", async ({ user }) => {
  let targetRoomTitle = `${user._id}.${Date.now()}`;
  const globalLockKey = "rooms-global-operation-lock";

  // 先获取全局锁,确保同一时间只有一个请求操作rooms对象
  await acquireLock(globalLockKey);

  try {
    // 必须在锁内部重新查找房间,因为等待锁的过程中,房间状态可能已经被其他请求修改了
    const foundRoomKey = Object.keys(rooms).find(roomKey => {
      const room = rooms[roomKey];
      return room.roomLimit > room.users.length;
    });

    if (!foundRoomKey) {
      // 创建新房间
      rooms[targetRoomTitle] = {
        title: targetRoomTitle,
        users: [{ _id: user._id }],
        roomLimit: 2,
        roomType: "public",
        status: "open",
      };
      socket.join(targetRoomTitle);
    } else {
      // 加入找到的房间
      const room = rooms[foundRoomKey];
      room.users.push({ _id: user._id });
      targetRoomTitle = room.title; // 更新目标房间标题,用于后续更新用户信息

      if (room.users.length === room.roomLimit) {
        room.status = "full";
        // 存储到MongoDB后删除内存中的房间
        if (await storeToMongoDB(room)) {
          delete rooms[foundRoomKey];
        }
      }
      socket.join(targetRoomTitle);
    }
  } finally {
    // 无论操作成功还是失败,都要释放锁,避免死锁
    releaseLock(globalLockKey);
  }

  // 最后更新用户的房间信息,这个操作放在锁外面不影响,因为房间标题已经确定了
  await User.findByIdAndUpdate(user._id, { joinedRoom: targetRoomTitle });
 });
};

关键细节说明

  1. 锁内重新查找房间:等待锁的过程中,其他请求可能已经修改了房间状态,所以必须在获取锁后重新执行查找,不能用之前的结果。
  2. try...finally保证锁释放:即使代码执行过程中抛出异常,锁也会被释放,不会导致后续请求永远等待。
  3. 用户更新放在锁外:数据库操作相对较慢,放在锁外可以减少锁的持有时间,提升并发性能。
  4. 修正用户更新的参数:原来的User.findByIdAndUpdate(user, ...)可能有问题,应该用user._id作为查询条件,确保正确找到用户。

这样修改后,不管多少用户同时发起请求,都会排队执行房间的操作,彻底解决超员的问题。

内容的提问来源于stack exchange,提问作者Siddhesh Nayak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 00:02:32