PHP项目直播在线人数如何实时统计?从基础实现到性能优化全指南
目录导读
- 核心原理:为什么实时人数统计这么难?
- 基于文件锁 + 定时刷新的简易方案
- Redis + Socket.IO 的真实时方案(推荐)
- WebSocket + 内存表 的高并发方案
- 常见问题与解决方案(QA)
- 性能对比与选择建议
核心原理:为什么实时人数统计这么难?
直播场景下,用户频繁进出、页面刷新、网络波动都会导致人数统计不准确,PHP 作为传统同步语言,默认的请求-响应模式天然不适合实时推送。关键难点在于:

- 用户断开连接时,如何及时扣除在线人数?
- 如何避免因并发写入导致的数据不一致?
- 如何支撑万人直播场景下的高轮询频率?
解决方案本质:我们需要一个能持续追踪用户状态(心跳/连接/离开)的中间层,配合高效的计数器存储。
方案一:基于文件锁 + 定时刷新的简易方案
适用场景:小型活动、内测阶段、日活 < 1000 的直播。
实现思路
- 用文件或数据库存储当前在线用户 ID 列表
- 每次用户发请求时,更新该用户的时间戳
- 定时脚本清理超时的用户(如30秒无心跳则下线)
核心代码示例(PHP + 文件锁):
$file = '/tmp/live_count.txt';
$lock = fopen($file, 'c+');
flock($lock, LOCK_EX);
$data = json_decode(file_get_contents($file), true);
$user_id = $_GET['user_id'];
$data[$user_id] = time(); // 更新时间戳
// 清理30秒无心跳用户
foreach ($data as $uid => $ts) {
if (time() - $ts > 30) unset($data[$uid]);
}
file_put_contents($file, json_encode($data));
fclose($lock);
echo count($data);
局限:
- 无法主动通知前端人数变化
- 文件锁在并发 > 500 时性能急剧下降
- 接口无法区分“页面关闭”与“停留未操作”
方案二:Redis + Socket.IO 的真实时方案(推荐)
适用场景:中小型直播平台,需亚秒级延迟,支持万级同时在线。
架构设计
客户端 A → [Socket.IO 长连接] → Node.js 服务 → Redis SortedSet/Hash → PHP 后端消费
客户端 B ← [WebSocket 推送] ← Node.js 服务 ← Redis Pub/Sub ← PHP 业务逻辑
具体实现步骤
步骤1:客户端心跳检测
// 前端使用 Socket.IO 发送心跳
const socket = io('wss://your-domain.com');
socket.emit('joinRoom', { roomId: 'live_123' });
// 每15秒发送ping
setInterval(() => {
socket.emit('heartbeat', { roomId: 'live_123' });
}, 15000);
步骤2:服务端 Redis 存储
// PHP 使用 Predis 库维护在线集合 $redis = new Predis\Client(); $roomKey = 'live:room:123:users'; // 当收到心跳或加入消息时 $redis->zAdd($roomKey, time(), $userId); // 定时清理30秒无心跳的用户 $redis->zRemRangeByScore($roomKey, '-inf', time() - 30); // 获取当前在线人数 $onlineCount = $redis->zCard($roomKey);
步骤3:实时推送人数
// Node.js 中间层监听 Redis 变化
redisClient.subscribe('live:room:123:count');
redisClient.on('message', (channel, count) => {
io.to('live_123').emit('onlineCount', parseInt(count));
});
性能数据:单台服务器可支撑 5 万+ 同时在线(基于经验测试结果,实际因服务器配置而异)
方案三:WebSocket + 内存表 的高并发方案
适用场景:大型直播平台,需毫秒级响应与弹性扩缩容。
技术栈组合
- Swoole:PHP 协程 WebSocket 服务,直接管理客户端连接
- 内存表(Table):Swoole 提供的共享内存,无锁高性能
关键实现:
// Swoole WebSocket 服务器
$server = new Swoole\WebSocket\Server('0.0.0.0', 9501);
// 创建内存表存储用户连接
$table = new Swoole\Table(1024 * 100); // 支持10万用户
$table->column('room_id', Swoole\Table::TYPE_STRING, 32);
$table->column('last_heartbeat', Swoole\Table::TYPE_INT, 8);
$table->create();
$server->on('open', function ($server, $req) use ($table) {
// 建立连接时记录用户
});
$server->on('message', function ($server, $frame) use ($table) {
// 更新心跳时间
});
$server->on('close', function ($server, $fd) use ($table) {
// 清除用户,直接减1
$table->del($fd);
// 广播当前人数
});
优势:完全绕开 PHP 传统请求周期,WebSocket 连接即用户,断开即自动减1。
常见问题与解决方案(QA)
Q1:用户刷新页面会导致人数虚增吗?
A:会,解决方案:在前端 unload 事件时发送离开信号,但用户强制关闭标签页可能来不及发送。最佳实践:服务端采用心跳超时机制,即使客户端未主动断开,超过30秒无心跳也视为离开。
Q2:如何防止恶意刷人数?
A:三层防护策略:
- 用户维度:同一 IP 限制每分钟心跳次数
- 房间维度:Redis 加布隆过滤器检测异常涌入
- 业务维度:结合用户 token 验证身份,未登录用户不统计
Q3:Redis 宕机怎么办?
A:降级方案:
- 启用本地文件缓存作为备用计数器(精度下降但业务不中断)
- 使用 Redis 哨兵模式或集群模式保证高可用
- 定期将在线数据同步到 MySQL 持久化
Q4:PHP 传统框架(如 Laravel)能否实现实时统计?
A:可以,但需搭配 Laravel WebSocket 包(如 laravel-websockets),内部原理仍是使用 Ratchet 或 Swoole 驱动,本质是 PHP 作为 WebSocket 服务端而非传统请求响应。
性能对比与选择建议
| 方案 | 延迟 | 并发支持 | 开发成本 | 运维复杂度 |
|---|---|---|---|---|
| 文件锁方案 | 3-10秒 | < 1000 | 低 | 极低 |
| Redis+Socket | < 1秒 | 1-5万 | 中 | 中 |
| Swoole 内存表 | < 100ms | 10万+ | 高 | 高 |
推荐路线:
- 初版上线:使用方案一快速验证
- 增长期:迁移到方案二(最均衡)
- 规模化:方案三配合容器化调度
实时在线人数统计的本质是连接管理 + 状态同步,PHP 本身虽不擅长长连接,但通过 Redis 作为状态层、WebSocket 作为推送层,完全能承载大型直播业务,建议优先采用 Redis SortedSet 方案,兼顾开发效率和性能,如果你的项目已经支持 Swoole 扩展,直接使用内存表方案可获得最佳性能。