弹幕是视频平台最热闹的互动形态:用户在视频的某一秒看到一行滚动的评论从屏幕滑过,一瞬间的「在场感」让千万人像一起看片。但从系统设计视角,弹幕是高并发的实时写 + 高扇出的实时推 + 需要内容安全过滤 + 需要持久化回放的综合系统。本文按照系统设计面试的标准答题结构,设计一个支持千万级在线的视频弹幕系统。
一句话:弹幕系统的核心是「发的秒级送达 + 千万人同时可见」——写路径要扛住高峰弹幕洪峰,读路径要毫秒级推送到海量在线观众,两者靠 WebSocket 长连接与分片扇出串联。
一、需求澄清与量级估算
1.1 需求澄清
面试官给出题目「设计一个弹幕系统」后,先通过提问明确边界:
- 弹幕发送:用户在视频播放过程中发送弹幕,是文本还是支持表情/颜色/字号?
- 展示模式:滚动弹幕(从右到左)、顶部固定、底部固定,都要吗?
- 时效性:弹幕需要「实时」还是「同步到回放」?回放多久可见?
- 实时规模:多少用户同时在线的热门视频?(峰值决定扇出架构)
- 内容安全:弹幕是否要过滤(敏感词/广告/刷屏)?审核通过前能否展示?
- 统计:是否要弹幕量、弹幕时段热力等统计?
明确假设(面向面试的合理假设):
| 需求项 | 假设 |
|---|---|
| 弹幕形态 | 文本弹幕,支持颜色/字号,滚动 + 顶部固定 |
| 时效 | 发送到展示 < 1 秒;回放实时拉取历史弹幕 |
| 规模 | 全站在线 1000 万观众,热门视频单视频同时 100 万人在线 |
| 内容安全 | 先发后审(异步过滤),敏感词拦截,刷屏限频 |
| 统计 | 分钟级弹幕量、时段热力 |
| 持久化 | 弹幕需回放,视频生命周期内可查 |
1.2 量级估算
| 指标 | 估算值 | 推导 |
|---|---|---|
| 同时在线观众 | 1000 万 | 全站峰值 |
| 热门视频同屏 | 100 万 | 单视频最高并发 |
| 弹幕发送峰值 | 5 万条/秒 | 高能时刻(名场面/直播回放) |
| 单条弹幕扇出 | 100 万份 | 一屏观众都看到 |
| 全站推送总量 | 数十亿条/秒 | 峰值弹幕 × 扇出 |
| 弹幕持久化 | 日 2 亿条 | 覆盖全量视频 |
一句话:5 万条/秒写入 × 100 万扇出 = 推送总量数十亿条/秒,弹幕是「写得多、推得更多」的系统——扇出必须靠长连接通道分组与广播,绝不能逐用户建消息。
二、高层架构设计
┌─────────────────────────────┐ ┌───────────────────────────┐
│ 观众端(播放器) │ │ 视频 / 直播平台 │
│ 发弹幕 / 收弹幕 / 渲染轨迹 │ │ 视频元数据 / 播放会话 │
└─────────────┬───────────────┘ └─────────────┬─────────────┘
│ 发送请求(WS) │ 拉取回放弹幕
┌─────────────▼──────────────────────────────────▼─────────────┐
│ 弹幕接入网关(WebSocket 集群) │
│ 长连接管理 / 心跳保活 / 房间分组 / 连接配额与限频 │
└──────┬───────────────────────────────┬───────────────────────┘
│ 新弹幕事件 │ 房间推送
┌──────▼─────────┐ ┌────────▼──────────────────┐
│ 弹幕写服务 │ │ 实时推送服务(扇出) │
│ - 内容过滤 │ │ - 房间订阅表 / 连接映射 │
│ - 敏感词/限频 │ │ - 分片广播 / 批量下行 │
│ - 持久化(异步) │ │ - 连接池直推/推流网关 │
└──────┬─────────┘ └────────┬──────────────────┘
│ │
┌──────▼─────────┐ ┌────────▼────────────────┐
│ 存储层 │ │ 历史弹幕服务 │
│ Redis:房间 │ │ 分页/时间窗口拉取 │
│ 弹幕流/计数 │ │ 回放缓存 │
│ MySQL/HBase: │ └─────────────────────────┘
│ 弹幕明细/热力 │
└────────────────┘
整体拆为五个模块:
- 接入网关:WebSocket 长连接集群,管理千万级在线连接,按视频房间分组。
- 弹幕写服务:接收弹幕、内容过滤、限频、异步持久化。
- 实时推送服务:把新弹幕扇出到房间内所有连接,分片广播 + 批量下行。
- 历史弹幕服务:回放时按时间窗口/分页拉取。
- 存储:Redis(房间实时弹幕流、在线人数)、HBase/MySQL(持久化明细、热力)。
2.1 为什么选 WebSocket 长连接
弹幕是「双向、低延迟、高频」互动,推送通道的选择决定了扇出能力:
轮询(HTTP 短连接):延迟高、连接成本高,千万在线不可行
SSE:单向推送,不支持上行 → 上行还得另开通道
WebSocket:双向长连接、头部开销小、服务端可主动推 → 唯一务实选择
连接管理:
播放器进入视频 → 建连并注册到「房间」→ 心跳保活
连接配额:单网关可挂的连接上限 → 网关水平扩展支撑千万级
一句话:WebSocket 长连接让「推」成为可能——服务端能在弹幕产生的一瞬间把消息推进千万个已建立的信道,而不是让千万用户轮询拉取。
三、核心组件设计
3.1 弹幕模型与发送链路
弹幕是「视频维度的带时间戳短消息」:
CREATE TABLE danmaku (
danmaku_id BIGINT PRIMARY KEY, -- 雪花ID,全局递增
video_id BIGINT, -- 所属视频
user_id BIGINT,
content VARCHAR(512),
ts_ms INT, -- 视频时间轴(毫秒)
color VARCHAR(8),
mode TINYINT, -- 1滚动 2顶部固定 3底部固定
status TINYINT, -- 0待审 1展示 2屏蔽 3删除
created_at DATETIME
);
CREATE INDEX idx_video_ts ON danmaku (video_id, ts_ms);
发送链路:
观众输入 → 发送请求(带 video_id + ts_ms + content)
① 限频:同用户 N 秒内最多 M 条(防刷屏)
② 内容过滤:敏感词拦截 + 语义模型判定(命中即不展示)
③ 分配 ID:雪花ID(保证全局有序)
④ 推送:写 Redis 房间流 + 扇出到房间连接
⑤ 持久化:异步批量落库(回放用)
⑥ 统计:递增房间弹幕计数(热力)
3.2 房间分组与扇出
扇出是弹幕系统吞吐的命门。**房间(video_id)**是扇出单位,一个房间一条弹幕推给房间内所有连接:
房间模型:
Redis: room:{video_id}:online → 该房间连接(或连接所属网关)集合
Redis: room:{video_id}:stream → 实时弹幕流(滚动窗口,最近 N 条)
扇出路径(一次弹幕的旅程):
写服务 → Redis stream 追加
→ 房间订阅(网关集群按房间分发)
→ 网关本地批量下行到本网关上的连接
→ 播放器收到渲染
关键优化:
- 按「网关分组」分发,而不是逐用户:一个网关只收一份再本地广播
- 下行批量:同一网关多连接同帧合并写(提高吞吐)
- 缓冲削峰:网关下行队列缓冲,慢消费者不阻塞快的
;; 伪代码:按网关分组扇出
(defn fanout [room-id danmaku]
(let [gateways (redis/smembers (str "room:gw:" room-id))]
(doseq [gw gateways]
;; 每个网关一份消息,网关本地广播给其连接
(push-to-gateway gw {:room room-id :danmaku danmaku}))))
;; 网关侧:本地批量下行
(defn gateway-broadcast [conn-mapping msg]
(doseq [batch (partition-all 1000 conn-mapping)] ; 每批 1000 连接
(async/send-all! batch msg))) ; 合并写同帧
3.3 弹幕渲染:车道算法与流量控制
弹幕渲染在客户端,但系统设计要保证「不重叠、不卡屏」。车道算法是关键:
车道(Lane)模型:
屏幕高度均分 N 条滚动车道(如 12 条)
每条弹幕进入一条车道,从左往右匀速移动
车道占用:弹幕宽度 + 速度 → 预计离开时间 → 车道选择
分配策略:
① 顺序轮转(简单):按发送顺序轮流进车道 → 可能头尾相接
② 密度感知(更好):优先进入「当前负载低」的车道 → 减少重叠
③ 限流保护:同屏弹幕数超阈值 → 丢弃低频权重弹幕(不影响主体验)
客户端渲染约束:
单屏弹幕上限:如 50 条(超限按优先级丢弃)
弹幕字号/速度:统一基准,避免视觉噪声
暂停/跳转:seek 后从该时刻历史弹幕重新灌入
要点:弹幕「不重叠」本质是车道调度问题——服务端控制「发多少」与「发谁的」,客户端控制「怎么排」,两端配合才能在大弹幕量下保持可读性。
3.4 内容安全过滤
弹幕是实时内容,过滤要「快」且「不阻塞展示」:
两级过滤:
① 同步轻过滤(毫秒级):敏感词精确/变体命中 → 直接拦截不展示
② 异步重过滤:语义模型 + 人工抽审 → 命中后撤回/隐藏该弹幕
限频:同用户高频发送触发冷却(防刷屏)
工程要点:
敏感词库:分级词库(高危直接拦,中危进重过滤)
过滤结果落库:status 字段驱动展示/屏蔽切换
撤回机制:已展示弹幕被判定违规 → 下发「撤回指令」删除客户端缓存
3.5 历史弹幕与回放
观众进入视频瞬间看不到已发生的弹幕,需要拉历史:
冷启动拉取:
进房间 → 立即拉「最近 N 条弹幕」(redis room stream)→ 快速铺屏
再按需拉「从视频开头到当前时刻」的分页历史(覆盖 ts_ms 窗口)
回放数据路径:
Redis 只存最近窗口(滚动),全量历史走持久化存储
分页查询:video_id + ts_ms 窗口 + 分页游标(idx_video_ts 索引)
seek 场景:按目标时刻 ts 分页拉取该窗口弹幕
;; 伪代码:历史弹幕分页
(defn fetch-history [video-id from-ts cursor limit]
(SELECT * FROM danmaku
WHERE video_id = ? AND ts_ms >= ? AND status = 1
AND danmaku_id < ? -- 游标分页(ID 有序)
ORDER BY ts_ms, danmaku_id
LIMIT ?))
四、深入权衡
4.1 Redis 房间流 vs 全量扇出
| 方案 | 机制 | 适用 |
|---|---|---|
| Redis Stream/List 房间流 | 写一次,房间成员各自消费 | 回放、冷启动 |
| 网关直接扇出 | 写一次,网关按分组广播 | 实时推送热路径 |
结论:实时推送走「网关分组扇出」(低延迟热路径),回放冷启动走「Redis 房间流」(一次写入多消费)——两者互补,而不是互相替代。
4.2 弹幕持久化选型
| 存储 | 优势 | 短板 |
|---|---|---|
| HBase(宽表) | 海量追加写、按 key 高效扫 | 运维复杂 |
| MySQL 分库分表 | 事务、索引成熟 | 写入瓶颈、需分片 |
| Kafka 桥接数仓 | 高吞吐缓冲 | 实时查询需再落地 |
结论:弹幕是「高写低更新」的追加型数据,首选 HBase 或「Kafka 缓冲 + MySQL/HBase 落地」组合;面试侧重讲清「写入要扛 5 万/s,查询是按 video+ts 的范围扫描」即可。
4.3 内容过滤的实时性 vs 准确性
先发后审带来「违规弹幕可能已展示一瞬间」的风险,全审(先审后发)又会拖慢实时性:
方案:分层策略
- 高危词/广告:同步拦截(零展示延迟风险)
- 一般内容:先展示 + 异步重过滤 + 撤回指令
- 重大活动/直播:降级为先审后发(保安全牺牲体验)
一句话:弹幕过滤的取舍是「同步拦高危、异步纠一般、重大场景降级全审」——用撤回机制把异步过滤的失误补救回来,兼顾实时与安全。
五、总结
弹幕系统的技术主线是写得进、推得出、画得开、回得去:写路径用「限频 + 敏感词拦截 + 雪花 ID + 异步持久化」扛住 5 万条/秒的发送洪峰;推路径用「WebSocket 长连接 + 房间分组 + 网关级扇出 + 批量下行」让一条弹幕在毫秒内抵达百万观众;渲染用「车道算法 + 单屏上限」保证不重叠不卡屏;回放用「Redis 房间流冷启动 + 持久化分页历史」。内容安全以「同步拦高危、异步重过滤 + 撤回」守住底线。弹幕系统的本质,是把「一人一句话」放大成「千万人共享的在场感」,而系统设计要做的,就是让这份热闹来得快、来得准、来得干净。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。