如何解决Socket.io中创建/加入单房间限2人时的并发请求问题
解决并发请求下房间超员的问题
你碰到的这个问题是典型的竞态条件导致的——多个用户同时发起请求时,它们会同时读取到同一个有空位的房间状态,接着都执行加入操作,最终突破了2人的房间限制。因为内存里的rooms对象没有任何并发控制,查找房间和修改房间状态这两步不是原子操作,中间的空隙就会被并发请求钻空子。
问题分析
看你的代码逻辑:
- 先遍历
rooms查找有空位的房间 - 如果找到就加入,没找到就创建新房间
但这两步之间没有任何同步机制,当两个请求同时完成第一步的查找,都发现某个房间还有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 }); }); };
关键细节说明
- 锁内重新查找房间:等待锁的过程中,其他请求可能已经修改了房间状态,所以必须在获取锁后重新执行查找,不能用之前的结果。
try...finally保证锁释放:即使代码执行过程中抛出异常,锁也会被释放,不会导致后续请求永远等待。- 用户更新放在锁外:数据库操作相对较慢,放在锁外可以减少锁的持有时间,提升并发性能。
- 修正用户更新的参数:原来的
User.findByIdAndUpdate(user, ...)可能有问题,应该用user._id作为查询条件,确保正确找到用户。
这样修改后,不管多少用户同时发起请求,都会排队执行房间的操作,彻底解决超员的问题。
内容的提问来源于stack exchange,提问作者Siddhesh Nayak
相关产品推荐
相关产品推荐

