本文目录导读:

- 核心策略:分层架构
- 方案一:基于 Swoole/Workerman(推荐,最主流)
- 方案二:基于 Nginx + SSE (Server-Sent Events) + Redis
- 方案三:传统 PHP-FPM + Redis 轮询(不推荐但常见)
- 关键优化点
处理高并发弹幕在 PHP 中是一个经典挑战,因为 PHP 传统的请求-响应模型(每次请求创建和销毁进程/线程)在处理长连接和大量并发写入时性能较差。
高并发弹幕的核心需求是:低延迟写入、高吞吐分发、海量连接保持。
针对 PHP 环境,业界通常采用 “PHP 作为业务逻辑层,底层使用高性能组件” 的架构,以下是详细的处理方案和代码示例:
核心策略:分层架构
绝不使用 PHP 原生的 file_put_contents 或 MySQL 直接写库来处理弹幕,这会在高并发下瞬间崩溃。
正确的做法是:
- 接入层:Nginx 处理并发请求。
- 写入层:PHP 将弹幕数据快速写入 Redis(或 Kafka)。
- 分发层:使用专业的实时推送服务(如 Workerman、Swoole WebSocket)或 SSE(Server-Sent Events)将弹幕推送给用户。
- 持久化:异步将 Redis 中的数据同步到 MySQL 做历史记录。
基于 Swoole/Workerman(推荐,最主流)
这是处理高并发的最佳方案,PHP 常驻内存,通过 WebSocket 保持长连接,摆脱了传统 PHP 生命周期限制。
架构流程
- 客户端:通过 WebSocket 连接服务器。
- PHP (Swoole/Workerman):监听 9501 端口作为 WebSocket 服务端。
- Redis:作为消息队列(PUB/SUB 或 List)。
- 流程:
- 用户 A 发送弹幕。
- PHP 协程/进程接收消息。
- 将消息写入 Redis List (用于持久化) 并
publish到 Redis 频道。 - 所有在线的 PHP 连接(或通过 Redis 订阅的客户端)接收该消息并推送给所有连接的客户端。
代码示例(Swoole WebSocket + Redis)
<?php
// server.php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
$server = new Server("0.0.0.0", 9501);
// 创建一个 Redis 客户端用于订阅
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 设置订阅回调(在子进程中)
$server->on('workerStart', function ($server, $workerId) {
// 仅在一个 Worker 进程中订阅,避免重复
if ($workerId == 0) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 模式订阅
$redis->psubscribe(['danmu_channel'], function ($redis, $pattern, $chan, $msg) use ($server) {
// 收到 Redis 消息,广播给所有 WebSocket 客户端
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) {
$server->push($fd, $msg);
}
}
});
}
});
// 客户端连接时
$server->on('open', function (Server $server, Request $request) {
echo "连接成功: {$request->fd}\n";
});
// 客户端发送弹幕时
$server->on('message', function (Server $server, Frame $frame) {
$data = json_decode($frame->data, true);
$msg = ['user' => $data['user'] ?? '匿名', 'content' => $data['content'] ?? '', 'time' => time()];
// 1. 异步写入 MySQL(通过队列)或直接写入 Redis List 等待批量落库
// 伪代码:Redis 存入队列用于持久化
$redis_push = new Redis();
$redis_push->connect('127.0.0.1', 6379);
$redis_push->lPush('danmu_queue', json_encode($msg));
// 2. 发布到频道,触发订阅回调进行广播
$redis_push->publish('danmu_channel', json_encode($msg));
});
// 客户端断开时
$server->on('close', function ($ser, $fd) {
echo "客户端-{$fd} 断开连接\n";
});
$server->start();
// 前端代码 (发送和接收)
let ws = new WebSocket("ws://your_server:9501");
ws.onopen = function() {
console.log("Connected");
};
ws.onmessage = function(event) {
let data = JSON.parse(event.data);
// 在页面上滚动显示弹幕
showDanmu(data.content);
};
function sendDanmu(content) {
ws.send(JSON.stringify({user: "John", content: content}));
}
基于 Nginx + SSE (Server-Sent Events) + Redis
如果不想使用 WebSocket,或者业务逻辑偏向于“服务器单向推送弹幕给用户”,SSE 是一个轻量级的选择,它基于 HTTP,实现简单。
架构
- 用户发送弹幕:普通的 HTTP POST 请求到 Nginx/PHP。
- PHP 接收:写入 Redis List。
- 用户接收弹幕:浏览器发起 SSE 请求(HTTP 长连接)。
- PHP 处理 SSE:协程方式监听 Redis 频道并输出数据。
代码示例
<?php
// sse_server.php (基于 Swoole HTTP Server 实现 SSE)
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
$server = new Server("0.0.0.0", 9502);
// 处理 SSE 连接请求
$server->on('request', function (Request $request, Response $response) {
if ($request->server['request_uri'] == '/events') {
// 设置 SSE 头
$response->header('Content-Type', 'text/event-stream');
$response->header('Cache-Control', 'no-cache');
$response->header('Connection', 'keep-alive');
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 订阅频道
$redis->subscribe(['danmu_channel'], function ($redis, $chan, $msg) use ($response) {
// 向客户端推送
$response->write("data: {$msg}\n\n");
});
} else {
// 处理发送弹幕
if ($request->server['request_uri'] == '/send') {
$content = $request->post['content'];
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->publish('danmu_channel', json_encode(['content' => $content]));
$response->end("ok");
}
}
});
$server->start();
传统 PHP-FPM + Redis 轮询(不推荐但常见)
如果公司技术栈老旧,无法使用 Swoole,只能使用 Apache/PHP-FPM,可以通过 Redis + 前端轮询 或 Nginx 的 Stream 模块 勉强支撑中低并发。
核心逻辑
- 写入:客户端 POST 弹幕 -> PHP 写入 Redis List。
- 读取:前端 JS 每隔 2-3 秒请求一次
get_danmu.php。 - PHP 读取:
lRange获取最新的 N 条弹幕并返回 JSON。
优点:实现简单,兼容所有环境。 缺点:实时性差(有 2-3 秒延迟),请求量大,数据库压力高。
关键优化点
无论采用哪种方案,以下优化都是必须的:
-
异步化处理(关键):
- 弹幕写入后端 API 后,PHP 必须立即返回(
echo+fastcgi_finish_request()),将耗时的落库和分发操作放入 Redis 队列,由后台任务(如 Laravel Queue、Workerman 进程)异步处理。 - 绝对不能在请求周期内直接操作 MySQL 写库。
- 弹幕写入后端 API 后,PHP 必须立即返回(
-
Redis 队列削峰:
- 使用 Redis List 结构作为消息队列。
- 消费端(处理落库的进程)可以自己控制速度,避免高并发瞬间压垮数据库。
-
负载均衡与限流:
- 使用 Nginx
limit_req_zone对发送弹幕的 IP 进行限流(1 秒 1 条)。 - 使用防火墙/网关层过滤恶意攻击。
- 使用 Nginx
-
静态资源分离:
弹幕的 CSS、JS 文件使用 CDN 加速,减轻 Nginx 压力。
- 单机 PHP-FPM 无法支撑高并发弹幕(千万级用户)。
- 首选方案:使用 Swoole 或 Workerman 搭建 WebSocket 服务,配合 Redis 消息订阅分发,这是目前 B 站、斗鱼等主流站点的通用模式(虽然他们底层通常是 Go 或 C++,但 PHP 完全可以通过 Swoole 达到万级并发)。
- 附加建议:如果项目预算允许,可以考虑将 PHP 服务只做 RESTful API(写入队列),直接用 Nginx + Redis (lua-resty-redis) 做长连接推送,这样性能和并发能力会进一步提升。