并发创建房间问题的两种工程化解法
在实时对战、匹配类系统中,「创建房间 + 加入房间」是非常核心的一条链路。但只要这条链路中 存在会让出协程的操作(DB、RPC、跨节点加载实体等),并发问题就几乎不可避免。
一个典型现象是:
多个玩家几乎同时请求加入房间
系统判断“当前没有未满房间”
最终却创建出了多个未满房间
这并不是代码写错了,而是并发 + 协程让出下的必然结果。
下面总结两种在实际工程中可落地、可维护的解决思路。
问题的本质
核心矛盾只有一个:
创建房间的过程会让出协程,而“是否需要创建房间”的判断又不是原子操作。
只要多个请求在同一时间窗口内进入判断逻辑,就可能得到相同的结果,从而并发创建房间。
解决问题的关键不在于“写得更快”,而在于约束并发路径。
方法一:固定 hived + 调度系统串行化
这是最通用、最稳妥的一种方案,适合大多数实时房间系统。
hived: 是一个负责创建/加入房间的服务, 也管理未满房间,每个节点都有一个副本。
1. 创建 + 加入房间固定在一个 hived
第一步,是明确房间的管理归属。
约定规则:
- 房间由某个 hived 创建
- 该房间的「未满 / 可加入」状态,只能由创建它的 hived管理
- 其他节点的 hived 不参与这个房间的调度与占用
这样可以避免:
- 多个 hived 同时管理同一个房间
- 虚位占用状态分散在多个节点
- 房间状态不一致的问题
这是一个必要的架构前提。
2. 使用调度系统,串行化 Join 请求
即使房间只归属一个 hived,仍然存在问题:
创建房间需要 DB / RPC / 加载实体
这些都会让出协程
解决办法不是“避免 yield”,而是:
在 hived 内部,把 Join 请求队列化,强制串行执行。
调度系统的核心思想
- Join 请求不直接执行
- 封装为
JoinTask - 所有任务进入本地队列
- 同一时间只跑一个任务
- 即使任务中途 yield,也不会有其他 JoinTask 并发执行
调度系统伪代码
JoinScheduler(调度器)
JoinScheduler = {
queue = {},
running = false,
}
function JoinScheduler:push(task)
table.insert(self.queue, task)
if not self.running then
self.running = true
fork(function()
self:_run()
end)
end
end
function JoinScheduler:_run()
while true do
local task = table.remove(self.queue, 1)
if not task then
self.running = false
return
end
task:run(self)
end
end
要点:
- fork 只发生一次
- 一个调度循环处理完整个队列
- 队列空了才退出
JoinTask(状态机任务)
function JoinTask:run(ctx)
while true do
if self.state == "PICK" then
local room = ctx:pick_room(self.match_args)
if room then
self.room = room
self.state = "JOIN"
else
self.state = "CREATE"
end
elseif self.state == "CREATE" then
-- ⚠ 会让出协程
self.room = ctx:create_room(self.match_args)
self.state = "JOIN"
elseif self.state == "JOIN" then
-- ⚠ 会让出协程
local ok = self.room:join(self.player_id)
if ok then
self:done(true, self.room)
return
end
-- join 失败,重新 pick
self.state = "PICK"
end
end
end
关键点:
- create_room 和 join 都允许 yield
- 但 整个 JoinTask 是串行执行的
- 下一个 JoinTask 必须等当前任务彻底结束
效果
即使有 10 个玩家同时请求加入:
第 1 个任务创建房间(yield)
其余 9 个任务在队列中等待
房间创建完成后: 后续任务直接 join
最终只会创建 一个未满房间
方法一的总结
- 逻辑清晰
- 行为可预测
- 对 DB / RPC / 跨节点操作友好
- 非常符合 Skynet 的“单服务串行执行模型”
代价是:
- 加入请求在本地有一个极短的排队过程
但换来的是绝对可控的房间数量和状态一致性。
方法二:hived 收集请求,达标后再创建房间(纯内存方案)
这是另一种思路,偏向“匹配池”模型。
核心思想
在创建房间实体之前,所有操作都只发生在内存中。
流程变成:
- 玩家请求“获取空房间”
- hived 只做一件事:把玩家放入内存队列
- 当人数达到阈值(例如 10 人):
- 才真正创建房间实体
- 一次性把所有玩家加入房间
为什么这个方案天然安全?
- 在创建房间之前:
- 没有 DB
- 没有 RPC
- 没有跨节点
- 没有协程让出
- 所有逻辑都在一个服务、一个协程内完成
也就是说:
并发问题在房间创建前就被消灭了。
方法二的特点
优点:
- 性能极高
- 实现简单
- 天然避免多个未满房间
限制:
- 更适合「凑人数」的玩法
- 需要额外处理:
- 玩家超时
- 取消匹配
- 人数不足时的兜底策略
- 不太适合“随进随玩”的即时房间
两种方法的取舍
| 维度 | 方法一:调度系统 | 方法二:内存收集 |
|---|---|---|
| 是否解决并发 | 是 | 是 |
| 是否依赖调度 | 是 | 否 |
| 是否涉及协程让出 | 有 | 创建前无 |
| 适合玩法 | 即时房间 | 匹配型玩法 |
| 工程复杂度 | 中 | 中 |
总结
- 并发创建多个房间,并不是偶发 bug,而是设计问题
- 只要「创建房间」会让出协程,就必须对并发路径进行约束
- 工程上有两条可靠路线:
- 固定 hived + 本地调度串行化
- 先内存收集玩家,再创建房间
选择哪一种,取决于你的玩法形态,而不是个人偏好。
但无论选择哪种,都有一个共识:
不要指望并发条件下的“判断 + 创建”天然安全。
这是房间系统设计中,最容易踩坑、也最值得一开始就想清楚的地方。
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/1/01/27/
- 更新时间:2026-08-08 01:40:44
- 本文声明:转载请标记原文作者及链接