实时PHP项目:显示场上谁更占优势
这是一个常见的实时数据展示需求,下面我给你一套完整可落地的方案,涵盖架构、代码和实现细节。

场景分析
"场上谁更占优势"通常出现在:
- 体育比赛实时胜率/控球率
- 游戏对战双方战力对比
- 直播PK双方人气值
- 投票/竞猜实时态势
核心需求:前端秒级刷新 + 后端低延迟计算 + 数据可视化
技术架构
数据源 → Redis(实时缓存) → PHP(计算优势) → SSE/WebSocket → 前端
↑
MySQL(持久化)
推荐组合:
- Redis:存实时数值(毫秒级读写)
- PHP-FPM / Swoole:计算优势算法
- SSE (Server-Sent Events):PHP 最易实现的推送方案
- ECharts / Canvas:前端可视化
PHP 做 WebSocket 建议用 Swoole 或 Workerman;如果不想改架构,SSE 是最优解。
核心代码
优势值计算(后端 PHP)
<?php
// advantage.php - 计算谁更占优势
class AdvantageCalculator
{
/**
* 综合计算优势值
* @return array [red_score, blue_score, leading, gap]
*/
public static function calc(array $stats): array
{
// 各项权重(可按业务调整)
$weights = [
'score' => 0.4, // 得分
'control' => 0.25, // 控球/控制率
'attack' => 0.2, // 进攻
'defense' => 0.15, // 防守
];
$red = $blue = 0;
foreach ($weights as $key => $w) {
$r = $stats['red'][$key] ?? 0;
$b = $stats['blue'][$key] ?? 0;
$total = $r + $b;
if ($total > 0) {
$red += ($r / $total) * $w;
$blue += ($b / $total) * $w;
}
}
// 归一化到 0-100
$red = round($red * 100, 1);
$blue = round($blue * 100, 1);
$leading = $red > $blue ? 'red' : ($blue > $red ? 'blue' : 'draw');
$gap = round(abs($red - $blue), 1);
return [
'red' => $red,
'blue' => $blue,
'leading' => $leading,
'gap' => $gap,
'time' => date('H:i:s'),
];
}
}
Redis 实时数据
<?php
// redis_data.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
function pushStats($matchId, $redStats, $blueStats) {
global $redis;
$redis->hMSet("match:{$matchId}:red", $redStats);
$redis->hMSet("match:{$matchId}:blue", $blueStats);
$redis->publish("match:{$matchId}:update", json_encode(['ts' => time()]));
}
function getStats($matchId) {
global $redis;
return [
'red' => $redis->hGetAll("match:{$matchId}:red"),
'blue' => $redis->hGetAll("match:{$matchId}:blue"),
];
}
SSE 实时推送(推荐)
<?php
// sse.php - 服务端推送
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Nginx 关闭缓冲
set_time_limit(0);
$matchId = (int)($_GET['match_id'] ?? 1);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 可选:订阅 Redis 消息,有更新才推
$lastPush = 0;
while (true) {
if (connection_aborted()) break;
$stats = getStats($matchId);
$result = AdvantageCalculator::calc($stats);
// 节流:至少间隔 500ms
if (microtime(true) - $lastPush >= 0.5) {
echo "data: " . json_encode($result) . "\n\n";
@ob_flush();
@flush();
$lastPush = microtime(true);
}
usleep(200000); // 200ms 检查一次
}
前端展示
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">实时优势</title>
<style>
.bar { display:flex; height:40px; border-radius:8px; overflow:hidden; }
.red { background:#e74c3c; transition:width .4s; }
.blue { background:#3498db; transition:width .4s; }
.num { font-size:28px; font-weight:bold; }
.red-num { color:#e74c3c; }
.blue-num { color:#3498db; }
.leading { text-align:center; margin-top:10px; font-size:22px; }
</style>
</head>
<body>
<div class="bar">
<div class="red" id="redBar" style="width:50%"></div>
<div class="blue" id="blueBar" style="width:50%"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:8px">
<span class="num red-num" id="redNum">50</span>
<span class="num blue-num" id="blueNum">50</span>
</div>
<div class="leading" id="leading">势均力敌</div>
<script>
const matchId = 1;
const es = new EventSource(`sse.php?match_id=${matchId}`);
es.onmessage = (e) => {
const d = JSON.parse(e.data);
document.getElementById('redBar').style.width = d.red + '%';
document.getElementById('blueBar').style.width = d.blue + '%';
document.getElementById('redNum').textContent = d.red;
document.getElementById('blueNum').textContent = d.blue;
const el = document.getElementById('leading');
if (d.leading === 'red') el.textContent = '🔴 红方领先 ' + d.gap + '%';
else if (d.leading === 'blue') el.textContent = '🔵 蓝方领先 ' + d.gap + '%';
else el.textContent = '⚖️ 势均力敌';
};
es.onerror = () => console.warn('连接断开,浏览器会自动重连');
</script>
</body>
</html>
关键优化点
| 问题 | 方案 |
|---|---|
| PHP-FPM 阻塞 | SSE 用 set_time_limit(0);Nginx 加 X-Accel-Buffering: no |
| 并发连接数 | 用 Swoole/Workerman 协程,或加 Redis 订阅避免轮询 |
| 计算延迟 | Redis + 权重算法 O(1),无 DB 查询 |
| 前端卡顿 | 宽度 transition + 节流 500ms |
| 断线重连 | EventSource 自动重连;Last-Event-ID 补数据 |
| 多实例 | Redis Pub/Sub 广播更新 |
进阶:Swoole WebSocket 版
如果并发 > 1000,换 WebSocket:
// swoole_server.php
$server = new Swoole\WebSocket\Server("0.0.0.0", 9501);
$server->on('open', function($s, $req) {
$s->push($req->fd, json_encode(['msg' => 'connected']));
});
$server->on('message', function($s, $frame) {
// 客户端订阅某场比赛
$matchId = (int)$frame->data;
// 用定时器每 500ms 推优势值
Swoole\Timer::tick(500, function() use ($s, $frame, $matchId) {
$result = AdvantageCalculator::calc(getStats($matchId));
$s->push($frame->fd, json_encode($result));
});
});
$server->start();
部署建议
- Nginx 反代 SSE:
proxy_buffering off; proxy_read_timeout 24h; - Redis 单独实例,开 AOF
- 监控:Prometheus 抓连接数、推送延迟
- 降级:SSE 断了自动切到 5s 轮询
/api/advantage.php
需要我针对你的具体场景(比如足球赛、直播间 PK、游戏对战)给出更精确的权重算法或数据模型吗?告诉我具体指标就行。