← 每日面试题 学习看板

设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?

面试题/01实践场景题/实践场景题目/场景与业务实践/设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?.md

完整讲解(权威版)

最后更新:2026-10-28 · 基于第 2 次作答

L1 · 30 秒速记

结论:万人在线房间的一致性 = 状态集中存 Redis + 长连接推送(WebSocket/SSE)+ 跨服务器广播(Redis Pub/Sub)+ 并发原语防冲突(SETNX/Lua)+ 心跳保活。 关键词:状态集中 · 跨服务器广播 · SETNX+Lua 原子占位 · 心跳保活 · 强/最终一致选型 边界:麦位状态要强一致(同步广播),聊天消息才允许最终一致(MQ 异步);禁止客户端轮询。

L2 · 核心讲解

核心考点定位:考察"分布式实时系统"中状态一致性的完整设计——状态存哪、怎么广播、并发怎么防、一致性怎么选;面试官想看到"长连接 + 状态集中 + 跨服务器广播 + 并发原语"的完整链路,而不是只背 Redis 或 WebSocket 单个组件。本题高难度,重点是跨服务器的广播机制与麦位并发,这两处是大多数考生缺失的。

机制链(≥3 环):

  1. 用户长连接挂在不同 WS 网关 → 状态变更发生在服务器 B,网关 A 上的用户看不到:本地内存或单机推送无法跨服务器同步,必须引入广播总线(Redis Pub/Sub)把变更推给所有网关。
  2. 状态集中到 Redis → 网关做成无状态:在线人数用 INCR/DECR 原子计数、麦位用 SET 集合、排队用 LIST,任意网关都能直接读,横向扩容不再受会话内存限制。
  3. 多用户同时申请同一麦位 → 必须原子占位:SETNX/Lua 脚本保证"检查空位 + 占位"一步完成,否则"先查后写"两步之间会产生竞态,出现两人抢到同一麦位。
  4. 用户掉线或断网 → 需要心跳保活:客户端 30s 心跳、服务器 60s 超时清理,配合 EXPIRE 自动释放麦位并回退在线计数,避免死连接长期占用麦位。
  5. 广播路径选型 → 麦位走同步广播(强一致),聊天消息走 MQ(最终一致):麦位被用户实时感知,要用 Redis Pub/Sub(延迟 < 10ms);聊天消息允许秒级收敛,用 Kafka 持久化更可靠。

证据锚点:

对比与权衡:

方案正确性吞吐/成本失败模式适用边界
状态存各网关本地内存跨网关不一致高但无法横向扩展网关重启丢状态、无法路由单机小房间,不适用万人
状态存 Redis + Pub/Sub 广播强一致高,Redis 扛万级更新Pub/Sub 丢消息、网关断连麦位/在线人数等状态,本题主方案
状态存 Redis + MQ 异步广播最终一致高,削峰秒级延迟、消息重复需幂等聊天消息/弹幕,可接受短暂不一致
客户端轮询拉状态取决于拉取频率极低,QPS 打爆服务器连接风暴、服务器崩溃禁止使用,万人场景不可行

常见误区:

  1. 错误:用客户端轮询同步房间状态。→ 正确:用长连接 + 服务端推送(WebSocket/SSE)。→ 为什么:万人轮询 QPS = 在线人数 × 频率,直接打爆业务服务器,这是本题最关键的工程判断。
  2. 错误:房间状态放各网关本地内存。→ 正确:状态集中存 Redis,网关无状态化。→ 为什么:用户连接分散在多个网关,本地内存无法跨网关一致,且网关无法横向扩展。
  3. 错误:麦位申请用"先查后写"两步。→ 正确:SETNX 或 Lua 脚本原子占位。→ 为什么:两步之间有多用户竞态,会两人同抢一麦位;Lua 保证"检查 + 占位 + 广播"原子性。
  4. 错误:所有状态一律走 MQ 异步最终一致。→ 正确:麦位走同步广播强一致,聊天消息才走 MQ。→ 为什么:MQ 有秒级延迟,麦位用户能感知到不一致;按"用户可感知的实时"分层选型。

适用边界:麦位、在线人数这类用户能直接感知的状态要强一致——写 Redis 后同步广播(延迟 < 100ms);聊天消息、弹幕这类允许秒级收敛的走 MQ 最终一致(需幂等 + 去重)。如果房间状态要求绝对不丢且需审计,给 Redis 加 AOF 持久化,或改存 MySQL 并配合本地消息表兜底。若做到百万级并发(YY 语音、KOOK),自建成本过高,应直接引入 IM 云(融云/环信/腾讯云 IM)承担消息,麦位状态仍自建。

延伸追问(≥3)

  1. 万人在线的 WebSocket 连接怎么横向扩展?
  1. 麦位申请高并发怎么保证原子性?
  1. 强一致和最终一致怎么选型?
  1. 心跳保活如何避免死连接占麦?

面试者参考回答

【结论】 万人在线的核心是"状态集中存 Redis + 长连接推送 + 跨服务器广播 + 并发原语 + 心跳保活":麦位管理用 SETNX + Lua 保证原子性,在线人数用心跳保活 + 超时清理;跨网关状态变更用 Redis Pub/Sub 广播;禁止客户端轮询。

【三点】 第一,架构分层:WebSocket 网关(Netty)负责用户长连接,无状态化(状态放 Redis)便于横向扩展;房间状态服务管业务逻辑,状态全部存 Redis(在线人数 INCR/DECR、麦位 SET、排队 LIST);Redis Pub/Sub 负责跨服务器广播——用户连在网关 A、状态变更发生在服务器 B,B 发消息到 Pub/Sub,A 订阅后推送给本地连接。第二,麦位管理有并发陷阱:多用户同时申请同一麦位,用 SETNX mic:{micId} userId 原子占位,失败进排队队列(LIST),空位时 BLPOP 阻塞等待;Lua 脚本保证"检查空位 + 占位 + 广播"的原子性;用户下线/心跳超时触发释放,释放后通知排队用户。第三,在线人数用心跳保活:客户端 30s 心跳、服务器 60s 超时清理(EXPIRE 自动过期,避免死连接);新用户进房直接读 Redis 拿当前状态,之后通过 WebSocket 订阅状态变更秒级感知。不能用客户端轮询——万人 QPS 会打爆服务器。选型上麦位强一致(同步广播,< 100ms)、聊天消息最终一致(MQ,秒级收敛)。

【边界】 如果房间状态要求绝对不丢且需审计,给 Redis 加 AOF 持久化或改存 MySQL + 本地消息表兜底;如果做到百万级并发(YY 语音、KOOK),自建成本过高,应直接用 IM 云(融云/环信/腾讯云 IM)承担消息,麦位状态仍自建——核心是"麦位状态自建,聊天消息用云"。

作答历史

#日期评分本轮主要问题记录
12026-10-144/5未提分布式广播机制:用户长连接挂在不同服务器时,怎么广播状态变更(Redis Pub/Sub 或独立消息总线)第 1 次
22026-10-283/5未覆盖广播机制(用户长连接挂不同服务器时状态变更如何跨服务器广播)、未提麦位并发管理、未提在线人数心跳保活、未提一致性模型第 2 次

面试记录 2026-10-14(第 1 次)

题目:设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?

难度:high

标签:practice/business, architecture/realtime

评分:⭐⭐⭐⭐☆(4/5)

我的回答

直播间这个东西本身就是一个独立的个体。然后直播间状态、麦位信息、在线人数这些,属于直播间个人的、直播间的属性,独立的进行维护。当用户上线或者申请麦位的时候,检查直播间属性上的值,然后进行更新。这个更新使用 Redis 更新,然后同步的展现就可以了。

用户上线的时候,对应的用户在线人数进行原子增,就可以更新上线在线的人数。当用户下线的时候,对应的更新对应的 Redis 就可以实现了。有时候为了保证业务更新,可能会用到 MQ。当用户上线下线的时候,发对应的消息到 MQ,然后监听 MQ 的变更,对应的去更新房间状态,来达到数据的最终一致性;

万人直播间的数据更新,直接用 Redis 本身就可以,并不一定需要 MQ 再去异步处理。在 MQ 异步处理可以提升它更高的兼容性,但是 Redis 本身万人直播间的更新就可以扛得住。新用户获取信息的时候直接获取直播间的信息,这个信息的更新通过长连接,进行对应的实时更新,或者使用服务端推送给客户端进行更新;但是不能使用客户端轮询,这样的客户端轮询产生的并发会让业务服务器崩溃

点评

优点:

不足:

补充:

面试记录 2026-10-28(第 2 次)

题目:设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?

难度:high

标签:practice/business, architecture/realtime, general/knowledge, difficulty/high

评分:⭐⭐⭐☆☆(3/5)

我的回答

房间状态等做独立存储,在单独表中,麦位,在线人数等信息都做缓存记录,从缓存中拉取,这种数据都是实时变动的并不需要落库,需要落库的信息也先更新到缓存中,从缓存中拉取;出现数据更新时,使用长连接,去将更新数据推送到客户端,做数据更新,可以使用websocket也可以使用SSE做信息推送,推送给客户端,进行数据更新来保证房间状态在各个直播间都可以看到一致的数据;不能用前端轮询,万人直播间轮询一下qps的并发可能直接打爆机器;

点评

评分:⭐⭐⭐☆☆(3/5)

优点:

不足:

补充:

完整讲解见上方「完整讲解(权威版)」