最后更新:2026-10-28 · 基于第 2 次作答
结论:万人在线房间的一致性 = 状态集中存 Redis + 长连接推送(WebSocket/SSE)+ 跨服务器广播(Redis Pub/Sub)+ 并发原语防冲突(SETNX/Lua)+ 心跳保活。 关键词:状态集中 · 跨服务器广播 · SETNX+Lua 原子占位 · 心跳保活 · 强/最终一致选型 边界:麦位状态要强一致(同步广播),聊天消息才允许最终一致(MQ 异步);禁止客户端轮询。
核心考点定位:考察"分布式实时系统"中状态一致性的完整设计——状态存哪、怎么广播、并发怎么防、一致性怎么选;面试官想看到"长连接 + 状态集中 + 跨服务器广播 + 并发原语"的完整链路,而不是只背 Redis 或 WebSocket 单个组件。本题高难度,重点是跨服务器的广播机制与麦位并发,这两处是大多数考生缺失的。
机制链(≥3 环):
INCR/DECR 原子计数、麦位用 SET 集合、排队用 LIST,任意网关都能直接读,横向扩容不再受会话内存限制。SETNX/Lua 脚本保证"检查空位 + 占位"一步完成,否则"先查后写"两步之间会产生竞态,出现两人抢到同一麦位。EXPIRE 自动释放麦位并回退在线计数,避免死连接长期占用麦位。Redis Pub/Sub(延迟 < 10ms);聊天消息允许秒级收敛,用 Kafka 持久化更可靠。证据锚点:
room:{roomId}:online:在线人数计数器 key,用 INCR/DECR 原子增减,避免并发错乱。room:{roomId}:mics:麦位集合 key,用 SET/SADD 存当前上麦用户,麦位数量有限(4–10 个)。room:{roomId}:mics:queue:麦位排队队列 key,用 LIST 存申请失败的用户,空位时按序分配。SETNX:原子占位,key 不存在才写入;麦位申请用它保证"一个麦位一个用户"。EXPIRE:给在线会话设过期时间,服务器 60s 超时清理死连接,自动释放麦位。BLPOP:阻塞等待空位,适合麦位排队,空位出现时唤醒队首用户。Netty:WebSocket 网关的常用实现,负责维护海量长连接与心跳。Redis Pub/Sub:跨服务器广播机制,延迟 < 10ms;无持久化,适合状态类广播。Kafka:可靠、有持久化,适合聊天消息,延迟 < 100ms,可回放。WebSocket:全双工长连接,生产环境首选;SSE(Server-Sent Events)单向推送、HTTP 兼容。对比与权衡:
| 方案 | 正确性 | 吞吐/成本 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 状态存各网关本地内存 | 跨网关不一致 | 高但无法横向扩展 | 网关重启丢状态、无法路由 | 单机小房间,不适用万人 |
| 状态存 Redis + Pub/Sub 广播 | 强一致 | 高,Redis 扛万级更新 | Pub/Sub 丢消息、网关断连 | 麦位/在线人数等状态,本题主方案 |
| 状态存 Redis + MQ 异步广播 | 最终一致 | 高,削峰 | 秒级延迟、消息重复需幂等 | 聊天消息/弹幕,可接受短暂不一致 |
| 客户端轮询拉状态 | 取决于拉取频率 | 极低,QPS 打爆服务器 | 连接风暴、服务器崩溃 | 禁止使用,万人场景不可行 |
常见误区:
SETNX 或 Lua 脚本原子占位。→ 为什么:两步之间有多用户竞态,会两人同抢一麦位;Lua 保证"检查 + 占位 + 广播"原子性。适用边界:麦位、在线人数这类用户能直接感知的状态要强一致——写 Redis 后同步广播(延迟 < 100ms);聊天消息、弹幕这类允许秒级收敛的走 MQ 最终一致(需幂等 + 去重)。如果房间状态要求绝对不丢且需审计,给 Redis 加 AOF 持久化,或改存 MySQL 并配合本地消息表兜底。若做到百万级并发(YY 语音、KOOK),自建成本过高,应直接引入 IM 云(融云/环信/腾讯云 IM)承担消息,麦位状态仍自建。
SETNX mic:{micId} userId 原子占位,失败进 LIST 排队;Lua 脚本合并"检查 + 占位 + 广播";空位出现时 BLPOP 唤醒队首。占位要设 TTL 防持麦者异常。EXPIRE 自动过期释放麦位并回退在线计数;超时判定要容忍网络抖动。【结论】 万人在线的核心是"状态集中存 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)承担消息,麦位状态仍自建——核心是"麦位状态自建,聊天消息用云"。
| # | 日期 | 评分 | 本轮主要问题 | 记录 |
|---|---|---|---|---|
| 1 | 2026-10-14 | 4/5 | 未提分布式广播机制:用户长连接挂在不同服务器时,怎么广播状态变更(Redis Pub/Sub 或独立消息总线) | 第 1 次 |
| 2 | 2026-10-28 | 3/5 | 未覆盖广播机制(用户长连接挂不同服务器时状态变更如何跨服务器广播)、未提麦位并发管理、未提在线人数心跳保活、未提一致性模型 | 第 2 次 |
题目:设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?
难度:high
标签:practice/business, architecture/realtime
评分:⭐⭐⭐⭐☆(4/5)
直播间这个东西本身就是一个独立的个体。然后直播间状态、麦位信息、在线人数这些,属于直播间个人的、直播间的属性,独立的进行维护。当用户上线或者申请麦位的时候,检查直播间属性上的值,然后进行更新。这个更新使用 Redis 更新,然后同步的展现就可以了。
用户上线的时候,对应的用户在线人数进行原子增,就可以更新上线在线的人数。当用户下线的时候,对应的更新对应的 Redis 就可以实现了。有时候为了保证业务更新,可能会用到 MQ。当用户上线下线的时候,发对应的消息到 MQ,然后监听 MQ 的变更,对应的去更新房间状态,来达到数据的最终一致性;
万人直播间的数据更新,直接用 Redis 本身就可以,并不一定需要 MQ 再去异步处理。在 MQ 异步处理可以提升它更高的兼容性,但是 Redis 本身万人直播间的更新就可以扛得住。新用户获取信息的时候直接获取直播间的信息,这个信息的更新通过长连接,进行对应的实时更新,或者使用服务端推送给客户端进行更新;但是不能使用客户端轮询,这样的客户端轮询产生的并发会让业务服务器崩溃
优点:
不足:
补充:
SADD + 唯一索引 防并发(SETNX 占位)List),有空位时按顺序分配INCR/DECR 原子操作题目:设计一个支持万人在线的语音聊天室,如何保证房间状态(如麦位信息、在线人数)的实时一致性?
难度:high
标签:practice/business, architecture/realtime, general/knowledge, difficulty/high
评分:⭐⭐⭐☆☆(3/5)
房间状态等做独立存储,在单独表中,麦位,在线人数等信息都做缓存记录,从缓存中拉取,这种数据都是实时变动的并不需要落库,需要落库的信息也先更新到缓存中,从缓存中拉取;出现数据更新时,使用长连接,去将更新数据推送到客户端,做数据更新,可以使用websocket也可以使用SSE做信息推送,推送给客户端,进行数据更新来保证房间状态在各个直播间都可以看到一致的数据;不能用前端轮询,万人直播间轮询一下qps的并发可能直接打爆机器;
评分:⭐⭐⭐☆☆(3/5)
优点:
不足:
补充:
SETNX mic:{micId} userId 原子占位 + Lua 脚本保证"检查 + 占位 + 广播"原子性;失败进 LIST 排队,空位时 BLPOP 唤醒;用户下线/心跳超时自动释放EXPIRE 自动过期),避免死连接占麦、在线人数虚高完整讲解见上方「完整讲解(权威版)」