PHP 怎么处理高并发弹幕

wen PHP项目 2

本文目录导读:

PHP 怎么处理高并发弹幕

  1. 核心策略:分层架构
  2. 方案一:基于 Swoole/Workerman(推荐,最主流)
  3. 方案二:基于 Nginx + SSE (Server-Sent Events) + Redis
  4. 方案三:传统 PHP-FPM + Redis 轮询(不推荐但常见)
  5. 关键优化点

处理高并发弹幕在 PHP 中是一个经典挑战,因为 PHP 传统的请求-响应模型(每次请求创建和销毁进程/线程)在处理长连接和大量并发写入时性能较差。

高并发弹幕的核心需求是:低延迟写入高吞吐分发海量连接保持

针对 PHP 环境,业界通常采用 “PHP 作为业务逻辑层,底层使用高性能组件” 的架构,以下是详细的处理方案和代码示例:


核心策略:分层架构

绝不使用 PHP 原生的 file_put_contents 或 MySQL 直接写库来处理弹幕,这会在高并发下瞬间崩溃。

正确的做法是:

  1. 接入层:Nginx 处理并发请求。
  2. 写入层:PHP 将弹幕数据快速写入 Redis(或 Kafka)。
  3. 分发层:使用专业的实时推送服务(如 Workerman、Swoole WebSocket)或 SSE(Server-Sent Events)将弹幕推送给用户。
  4. 持久化:异步将 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 模块 勉强支撑中低并发。

核心逻辑

  1. 写入:客户端 POST 弹幕 -> PHP 写入 Redis List。
  2. 读取:前端 JS 每隔 2-3 秒请求一次 get_danmu.php
  3. PHP 读取lRange 获取最新的 N 条弹幕并返回 JSON。

优点:实现简单,兼容所有环境。 缺点:实时性差(有 2-3 秒延迟),请求量大,数据库压力高。


关键优化点

无论采用哪种方案,以下优化都是必须的:

  1. 异步化处理(关键)

    • 弹幕写入后端 API 后,PHP 必须立即返回(echo + fastcgi_finish_request()),将耗时的落库和分发操作放入 Redis 队列,由后台任务(如 Laravel Queue、Workerman 进程)异步处理。
    • 绝对不能在请求周期内直接操作 MySQL 写库。
  2. Redis 队列削峰

    • 使用 Redis List 结构作为消息队列。
    • 消费端(处理落库的进程)可以自己控制速度,避免高并发瞬间压垮数据库。
  3. 负载均衡与限流

    • 使用 Nginx limit_req_zone 对发送弹幕的 IP 进行限流(1 秒 1 条)。
    • 使用防火墙/网关层过滤恶意攻击。
  4. 静态资源分离

    弹幕的 CSS、JS 文件使用 CDN 加速,减轻 Nginx 压力。


  • 单机 PHP-FPM 无法支撑高并发弹幕(千万级用户)。
  • 首选方案:使用 SwooleWorkerman 搭建 WebSocket 服务,配合 Redis 消息订阅分发,这是目前 B 站、斗鱼等主流站点的通用模式(虽然他们底层通常是 Go 或 C++,但 PHP 完全可以通过 Swoole 达到万级并发)。
  • 附加建议:如果项目预算允许,可以考虑将 PHP 服务只做 RESTful API(写入队列),直接用 Nginx + Redis (lua-resty-redis) 做长连接推送,这样性能和并发能力会进一步提升。

抱歉,评论功能暂时关闭!