今天我们来深入探讨一个经典的后端系统设计问题:如何设计一个能够支持百万级用户同时在线的直播评论系统,类似B站或抖音的弹幕功能。这类问题不仅考察技术细节,更考验面对海量并发场景时,对实时消息、数据存储、服务扩展和系统高可用性等方面的全局架构思考能力。
下面,我们将通过模拟面试问答的形式,一步步剖析和构建这个系统的核心架构。
1. 需求梳理与边界界定
面试官:“我们来聊聊系统设计吧。如果要你来设计一个类似 B站 的直播评论系统,你会怎么着手?”
面对系统设计问题,第一步永远是明确需求,划定系统边界。
“好的,面试官。对于一个直播评论系统,我们需要明确其核心的功能需求和在特定规模下的非功能性需求。”
1.1 功能需求
核心需求:
- 发布评论:观众可以在正在直播的视频中发布自己的评论。
- 实时查看:正在观看直播的观众,应该能近乎实时地看到新发布的评论。
- 历史评论:新加入直播间的观众,也应该能看到在他们加入之前已经发布的历史评论。
非核心需求:
- 评论的回复功能(即楼中楼)。
- 对评论进行点赞或送出表情回应。
1.2 非功能性需求
明确了“做什么”之后,需要确认系统的规模预期。
面试官:“假设我们的目标是支持大型活动,可能会有数百万观众同时在线,每个直播间每秒都可能产生数千条评论。”
这个量级将设计的挑战性直接拉满。基于此,可以推导出具体的非功能性需求:
核心需求:
- 高扩展性:系统需要具备水平扩展能力,以支持数百万并发观众,以及每秒数千条评论的高吞吐量。
- 高可用性:系统应优先保障可用性。在分布式环境下,可以接受最终一致性,因为评论的短暂延迟或顺序上的微小差异是可以容忍的。
- 低延迟:评论需要以近乎实时的方式广播给所有观众,端到端的延迟应控制在200毫秒以内。
非核心需求:
- 安全性:如用户认证、授权等。
- 内容完整性:如评论内容的审核、反垃圾等。
我们可以将需求整理成下表:

2. 底层设计
面试官:“需求很清晰。接下来你打算如何进行底层设计?”
“我倾向于从宏观到微观,先定义系统的核心实体,这有助于理清基本构成要素和关系,为后续的API设计和数据建模打下基础。”
2.1 核心实体定义
在这个问题中,我们只需要关注三个核心实体:
- 用户 (User):系统的参与者。
- 直播视频 (Live Video):评论所依附的主体。
- 评论 (Comment):用户在特定直播视频中发布的消息文本。
2.2 系统接口设计
实体明确后,可以为每个功能需求设计对应的API接口。
首先,需要一个接口来让用户发布评论。
// 向指定的直播视频流中创建一条新评论
POST /comments/:liveVideoId
// 请求头中需要包含用户身份认证信息
Header: JWT | SessionToken
// 请求体
{
"message": “这个直播太精彩了!”
}
注意:userId 应从请求头中的认证信息解析,而非通过请求体传递,这是防止身份伪造的标准实践。
其次,需要一个接口来获取历史评论。
// 获取指定直播视频的历史评论
GET /comments/:liveVideoId?cursor={last_comment_id}&pageSize=10&sort=desc
这个接口预设计了 cursor 和 pageSize 参数,表明我们考虑到了分页加载历史评论的场景。
3. 高层架构设计
有了接口定义,就可以开始搭建系统的高层架构了。让我们遵循敏捷思想,先从满足最核心的功能需求开始构建MVP。
3.1 观众如何发布评论?
面试官:“实体和接口定义清楚了。那么从架构层面看,我们先来解决第一个核心需求:如何让观众成功发布一条评论?”
“这个流程相对直接。我们可以设计一个三层架构:客户端、服务端和数据库。”

- 评论客户端 (Client):负责用户身份认证并将评论发送至后端服务。
- 评论服务端 (Service):负责接收、校验请求,并将评论数据持久化。
- 评论数据库 (Database):用于存储评论数据。可选用如DynamoDB、Table Store这类高可扩展的NoSQL数据库,或在初期使用 PostgreSQL、MySQL。
流程如下:
- 用户输入评论并点击发送。
- 客户端调用
POST /comments/:liveVideoId 接口。
- 评论服务端校验后,将评论存入 评论数据库。
至此,完成了基础的评论发布功能。但真正的挑战在于如何将评论展示给成千上万的观众。
3.2 观众如何看到新评论?
面试官:“很好,评论已经存进数据库了。那如何让直播间里所有其他观众都近乎实时地看到这条新评论呢?”
可以从最简单的方案开始思考:轮询。
“一个可行但效率较低的初级方案是,让每个观众的客户端每隔几秒向服务器发起一次请求,询问新评论。客户端带上最后看到的评论ID,服务端返回此后的新评论。”

“这个方案可扩展性极差。想象一个有100万观众的直播间,每人每3秒轮询一次,服务器将承受约33万QPS的压力,且绝大多数请求返回空结果,资源浪费巨大。因此,轮询方案不可行。”
3.3 观众如何看到历史评论?
面试官:“我同意轮询不是好办法。现在考虑另一个需求:新用户刚进入直播间时,如何看到之前的聊天记录?”
这里涉及对已存储数据的倒序展示,且数据量大,需要考虑分页。
“当用户加入直播时,他们需要立即开始接收实时新评论,并能够加载历史评论。对于历史评论,通常采用‘无限滚动’交互,这就需要我们的 GET /comments/:liveVideoId 接口支持高效的分页查询。主要有两种方案:偏移量分页 和 游标分页。”
-
偏移量分页(不推荐)
通过 offset 和 limit 查询,如 GET /comments/?offset=0&limit=10。
问题:随着数据增长,效率越来越低(需扫描跳过 offset 之前的所有行)。在高速写入场景下不稳定,新增数据会导致 offset 失效,可能引发重复或遗漏。
-
游标分页(推荐)
通过 cursor(如上一页最后一条记录的ID)定位,如 GET /comments/?cursor={last_comment_id}&pageSize=10。
一个在 DynamoDB 中使用游标查询的例子:
{
“TableName“: “comments“,
“KeyConditionExpression“: “liveVideoId = :liveVideoId AND commentId < :cursor“,
“ExpressionAttributeValues“: {
“:liveVideoId“: “liveVideoId_value“,
“:cursor“: “last_comment_id_value“
},
“ScanIndexForward“: false, // 按时间倒序查询
“Limit“: “pageSize_value“
}
优势:更高效(利用索引直接定位),且稳定(不受数据增删影响)。与 DynamoDB 的 LastEvaluatedKey 特性契合,扩展性更优。
结论:“因此,我们会选择基于游标的分页方案来加载历史评论,它在性能和稳定性上都更优。”
4. 高性能设计
面试官:“好的,分页方案很清晰。现在回到核心难题:如何实时地将评论广播给数百万观众?轮询显然不行,你有什么更好的方案?”
“是的,需要从客户端‘拉’数据的模式,转变为服务端‘推’数据的模式。实现这种模式主要有两种主流技术:WebSocket 和 服务器发送事件。”
4.1 如何确保评论能实时广播给观众?
4.1.1 WebSocket
建立双向通信通道,服务器在有新数据时主动推送。

“WebSocket能实现双向通知,但在我们读写极不均衡(读远多于写)的场景下,为每个只读观众维护一个双向通道开销较高。”
4.1.2 服务器发送事件 (SSE) (推荐)
建立持久的HTTP连接,允许服务器单向向客户端流式传输数据。写入(发布评论)仍通过普通HTTP POST完成。
优点:完美匹配场景(写入用POST,读取用SSE流)。基于标准HTTP,实现简单,支持断线重连。
挑战:部分代理或负载均衡器可能不支持流式响应;浏览器对同一域名并发SSE连接数有限制。

综合考虑,SSE 是我们这个读写极不平衡场景下的更优选择。
4.2 如何水平扩展SSE服务?
面试官:“选择SSE是合理的。但单台服务器无法维持百万长连接,必须水平扩展。那么问题来了:当评论发布请求落到服务器A时,它如何知道要将这条评论推送给连接在服务器B、C、D上的观众呢?”
这就是核心挑战:如何确保所有观众都能看到新评论,无论他们连接的是哪台服务器?
“最经典的解决方案是引入一个发布/订阅系统。”
4.2.1 负载均衡 + 简单Pub/Sub
创建独立的实时消息服务器来处理SSE连接。评论管理服务在新评论产生时,向一个公共频道发布消息。所有实时消息服务器都订阅该频道,收到消息后推送给各自连接的观众。

“这种方案虽然可行,但效率欠佳。每台实时消息服务器都需要处理所有评论,即便其并未转播该场直播,导致资源浪费和性能下降。”
4.2.2 分区发布/订阅 (Partitioned Pub/Sub)
根据直播视频ID (liveVideoId) 将评论流划分到不同的频道。每台服务器只订阅其连接观众所观看视频对应的频道。

“尽管更高效,但如果负载均衡器采用简单轮询,某台服务器仍可能连接到观看不同直播的观众,从而需要订阅多个频道。”
4.2.3 分区的Pub/Sub + 7层负载均衡器(推荐方案)
目标是:让观看同一直播的观众,尽可能连接到同一台实时消息服务器上。
通过支持第7层(应用层)的负载均衡器(如 NGINX, Envoy)实现。它可以检查请求中的 liveVideoId,并使用一致性哈希等策略,将同一视频的观众路由到同一台后端服务器。

进阶思考:“不同的Pub/Sub系统有取舍。例如,Redis Pub/Sub延迟低但‘发后即忘’;Kafka延迟稍高但提供持久化和更强投递保证。需根据业务容忍度选择。”
4.2.4 调度器替代Pub/Sub
引入一个调度器服务。评论管理服务收到新评论后,询问调度器该评论应发给哪台实时消息服务器,然后直接转发。

流程:
- 实时消息服务器上线时向调度器注册。
- 调度器维护
liveVideoId 到服务器的动态映射。
- 新评论产生时,评论管理服务查询调度器获取目标服务器并直接转发。
挑战:需确保调度器映射关系的准确性和高可用性。
总的来说,分区Pub/Sub + L7负载均衡的方案和调度器方案都是很好的思路。在面试中,Pub/Sub方案通常更简单,边界情况更少,可优先考虑。
5. 小结
至此,一个高并发、低延迟的直播评论系统的设计就基本完成了。回顾整个设计过程:
- 需求驱动:从最基本的功能和非功能需求出发。
- 迭代演进:从最简单的MVP开始,逐步识别瓶颈并引入更优方案(SSE、服务集群、Pub/Sub)。
- 权衡利弊:在每个技术选型点对比不同方案优劣,根据具体场景做出合理选择。
面试官看重的,正是这种结构化的思考能力、逐步分析问题的能力以及在不同方案间进行权衡决策的能力。
如果你对系统架构设计、高并发处理等后端核心技术感兴趣,欢迎到 云栈社区 与更多开发者交流学习,那里有丰富的后端 & 架构、数据库/中间件等专题讨论和资源分享。