本文目录导读:

- 方案一:经典 LNMP 架构(适合中小流量,普通虚拟主机)
- 方案二:基于 Swoole 的长连接方案(适合中等流量,独立服务器)
- 方案三:消息队列 + 高并发削峰(适合大型直播场景)
- 数据结构设计(数据库表)
- 关键细节与面试难点
设计一个 PHP 弹幕服务,核心在于高并发写入、低延迟读取以及消息推送,由于 PHP 本身是“请求-响应”模型,不适合常驻内存,因此设计时需要结合一些外部组件(如 Redis、Swoole)来弥补。
以下是针对不同业务规模的设计方案,从简单到复杂:
经典 LNMP 架构(适合中小流量,普通虚拟主机)
这种方案最容易实现,不需要额外的常驻进程,但性能有限。
架构流程:
前端 JS -> HTTP POST(发弹幕) -> PHP-FPM -> MySQL/Redis -> HTTP GET(取弹幕) -> 前端 JS
核心设计要点:
-
存储设计(结合 Redis + MySQL):
- Redis:使用
List或ZSet(按时间排序)存储最近 10 分钟的弹幕,作为热数据缓存,确保读取速度快。 - MySQL:作为冷存储,定期将 Redis 中的弹幕落盘,用于历史回放。
- Redis:使用
-
发送流程(写操作):
- PHP 接收请求后,校验内容(敏感词过滤、长度限制)。
- 将弹幕数据
LPUSH到 Redis 列表中。 - 定期(如每隔 5 分钟)将 Redis 数据迁移到 MySQL。
-
读取流程(读操作):
- 前端每隔 2-3 秒(轮询)请求 PHP 接口。
- 接口直接从 Redis 中
LRANGE取出该视频最近 N 条弹幕。 - 返回 JSON 格式数据。
-
代码示例(发送):
// send.php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $danmu = [ 'text' => $_POST['text'] ?? '', 'color' => $_POST['color'] ?? '#fff', 'time' => time() * 1000, 'uid' => $_SESSION['uid'] ?? 0 ]; // 生成视频专属弹幕队列 Key $key = 'video:danmu:' . intval($_POST['vid']); // 只保留最近 500 条在热数据中 $redis->multi() ->lPush($key, json_encode($danmu)) // 头部插入 ->lTrim($key, 0, 499) // 截断 ->expire($key, 600) // 设置 10 分钟过期 ->exec(); echo json_encode(['code' => 0]);
基于 Swoole 的长连接方案(适合中等流量,独立服务器)
由于 PHP 原生不支持长连接,使用 Swoole 扩展让 PHP 常驻内存,直接通过 WebSocket 推送消息,解决实时性问题。
架构流程:
客户端 <-> WebSocket <-> Swoole Server <-> Redis
核心设计要点:
-
独立服务:
- 编写一个
Swoole WebSocket服务端脚本,监听9501端口。 - 该进程常驻内存,维护所有在线用户的连接池。
- 编写一个
-
发布订阅(Pub/Sub):
- 当用户发送弹幕时,通过 HTTP 接口(或 WebSocket 事件)发给 Swoole 服务。
- 服务端将弹幕写入 Redis 列表,并执行
publish('danmu_channel', $data)广播。
-
房间(频道)管理:
- 每个视频(或直播间)是一个房间。
- 用户连接时传入
vid,服务端将其绑定到该房间的 Map 中。 - 只向当前房间的连接推送消息。
-
代码示例(Swoole 片段):
// server.php use Swoole\WebSocket\Server; $server = new Server("0.0.0.0", 9501); // 存储视频对应的 FD 连接 $rooms = []; $server->on('message', function ($server, $frame) use (&$rooms) { $data = json_decode($frame->data, true); $vid = $data['vid']; $rooms[$vid][] = $frame->fd; // 简单实例,真实需移除断线连接 // 构造弹幕数据 $danmu = ['text' => $data['text'], 'time' => time() * 1000]; // 写入 Redis 历史 $redis->lPush("video:danmu:$vid", json_encode($danmu)); // 推送给房间内的所有连接 foreach ($rooms[$vid] as $fd) { if ($server->exists($fd)) { $server->push($fd, json_encode($danmu)); } } }); $server->start();
消息队列 + 高并发削峰(适合大型直播场景)
如果遇到“秒杀”级别的弹幕风暴(如春晚、电竞决赛),PHP 进程可能会被瞬间打满。
架构流程:
客户端 -> Nginx/LB -> PHP-FPM -> Kafka/Redis Stream -> 消费者 -> 扇出(Fanout) -> WebSocket 集群
核心设计要点:
- 削峰填谷:写入操作用 Kafka(或 Redis Stream)替代直接操作 MySQL。
- 读写分离:读走 Redis 缓存,写走队列。
- WebSocket 集群:分布式部署 Swoole 服务,通过 Redis Pub/Sub 或 Kafka 进行跨节点消息广播,保证通过不同节点连接的用户也能同步收到消息。
数据结构设计(数据库表)
CREATE TABLE `danmu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `vid` int(11) NOT NULL COMMENT '视频ID', `uid` int(11) DEFAULT '0' COMMENT '用户ID', `content` varchar(500) NOT NULL COMMENT '弹幕内容', `color` varchar(10) DEFAULT '#FFFFFF' COMMENT '弹幕颜色', `position` tinyint(1) DEFAULT '0' COMMENT '滚动/顶部/底部', `show_time` float NOT NULL COMMENT '视频播放进度(秒)', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_vid_time` (`vid`, `show_time`) -- 关键索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键细节与面试难点
-
消息丢失怎么办? 确保 Redis 中存有数据,若 Redis 崩溃,需从 MySQL 持久化恢复,对于极高并发(如弹幕超出 50% 丢弃规则),需要有一个降级策略。
-
弹幕高并发下的“读扩散”问题? 通常指数量的增长,导致索引失效,建议将弹幕按时间分段(张列表)存储,
video:1000:seg:1表示前 10 秒的弹幕,前端只请求当前时间段的切片。 -
弹幕“防抖”与“限流”? 快速发送需要限流:单个用户 1 秒内只能发 1 条;单个用户发送频率过高,返回 429 错误码优化。
-
弹幕安全过滤? 正则匹配垃圾词、URL,或调用第三方内容安全接口。