本文目录导读:

- 方案一:基于数据库的“最后活动时间”(最基础、最常见)
- 方案二:基于 Redis 的缓存 + 心跳(推荐,性能与实时性平衡)
- 方案三:基于 WebSocket + 心跳包(实时性最强,现代应用)
- 方案四:SSE(Server-Sent Events)+ 数据库/缓存(轻量级实时方案)
- 总结与选型建议
在PHP项目中实现在线状态监测,有几种主流方案,具体选择取决于你的项目架构(传统单体、API驱动、还是实时性要求高的应用)。
以下是几种由简单到复杂、由传统到现代的方案:
基于数据库的“最后活动时间”(最基础、最常见)
原理:用户每次请求页面或接口时,更新数据库或缓存中的 last_activity_at 时间戳,判断用户是否在线,就看这个时间戳是否在“当前时间 - 预设的超时时间”范围内。
适用场景:传统Web应用、对实时性要求不高的后台管理系统。
实现步骤:
-
数据库设计:在
users表中添加字段。ALTER TABLE users ADD COLUMN last_activity_at TIMESTAMP NULL; ALTER TABLE users ADD COLUMN is_online TINYINT(1) DEFAULT 0;
-
中间件/钩子更新:在用户访问任何需要登录的页面或API时,更新该时间。
// 在你的Session中间件或基类控制器中 if (Auth::check()) { $user = Auth::user(); $user->last_activity_at = now(); $user->is_online = true; $user->save(); } -
检查在线状态:定义一个离线超时时间(例如5分钟)。
// 判断是否在线 function isUserOnline($user) { if (!$user->last_activity_at) return false; $timeout = 5; // 5分钟 return $user->last_activity_at->gt(now()->subMinutes($timeout)); } // 批量检查(例如首页显示用户列表) $users = User::all(); foreach ($users as $user) { $user->is_online_now = $user->last_activity_at && $user->last_activity_at->gt(now()->subMinutes(5)); }
优点:实现简单,不依赖额外服务。
缺点:对数据库写入频繁;并非真正的实时(有几分钟延迟);用户关闭浏览器后,状态更新可能不及时。
基于 Redis 的缓存 + 心跳(推荐,性能与实时性平衡)
原理:将用户在线状态存储在 Redis 中(Hash 或 Set 结构),利用 Redis 的 TTL(过期时间)自动管理离线状态。
适用场景:中大型项目、需要高并发读写、需要API快速响应的系统。
实现步骤:
-
Redis 数据结构:
- 方案A(Set + TTL):
EXPIRE user:online:{userId} 300,每次访问刷新。 - 方案B(Hash):
HSET user:online {userId} {timestamp},定时清理。
- 方案A(Set + TTL):
-
用户请求时更新(PHP伪代码):
use Illuminate\Support\Facades\Redis; class OnlineMiddleware { public function handle($request, $next) { if ($user = Auth::user()) { $userId = $user->id; $redisKey = "user:online"; // 使用有序集合(ZSet)存储,score为时间戳,方便范围查询 Redis::zadd($redisKey, time(), $userId); // 设置ZSet的过期(可选,但通常会保留) // 同时清理30秒前的旧数据(可选,防止ZSet过大) // Redis::zremrangebyscore($redisKey, 0, time() - 30); } return $next($request); } } -
检查在线状态(PHP伪代码):
function isUserOnline($userId) { $timeout = 30; // 30秒内活动视为在线 $score = Redis::zscore("user:online", $userId); if ($score === false) return false; return (time() - $score) <= $timeout; } // 获取所有在线用户ID function getAllOnlineUserIds() { $timeout = 30; $minTime = time() - $timeout; return Redis::zrangebyscore("user:online", $minTime, time()); }
优点:读写极快(内存操作),不增加数据库压力;可以轻松管理大量并发用户;状态过期自动清理。
缺点:需要额外维护 Redis 服务;需要考虑 Redis 宕机时状态丢失的问题(可降级回数据库方案)。
基于 WebSocket + 心跳包(实时性最强,现代应用)
原理:浏览器与服务器建立 WebSocket 长连接,客户端定时发送 ping 心跳包,服务器标记用户在线,连接断开或未收到心跳超时,标记离线。
适用场景:聊天应用、协作编辑、实时游戏、在线客服、股票行情。
实现步骤(以 Laravel + Pusher/Broadcasting 或 Node.js + Socket.IO 为例,PHP通常作为API后端):
-
选择技术栈:
- Pusher/Ably:第三方服务,简单但付费。
- Laravel WebSockets:基于 Laravel 的本地 WebSocket 方案(基于
ratchet或swoole)。 - Swoole/Workerman:高性能PHP常驻内存框架,原生支持 WebSocket。
- Node.js + Socket.IO:前端友好,PHP只负责API认证和存储状态(推荐分离架构)。
-
客户端(JavaScript):
// 连接WebSocket const socket = new WebSocket('wss://yourserver.com/ws'); // 登录后发送身份认证(例如通过token) socket.onopen = function() { socket.send(JSON.stringify({type: 'auth', token: 'xxx'})); }; // 心跳:每25秒发送一次 setInterval(() => { socket.send(JSON.stringify({type: 'ping'})); }, 25000); // 监听服务器返回的在线状态事件 socket.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === 'online_users') { console.log('当前在线用户ID:', data.user_ids); } }; -
服务端(PHP + Swoole 伪代码):
// 使用Swoole WebSocket服务器 use Swoole\WebSocket\Server; $server = new Server("0.0.0.0", 9501); // 存储在线用户映射 [fd => userId] $onlineUsers = []; $server->on('message', function ($server, $frame) use (&$onlineUsers) { $data = json_decode($frame->data, true); $fd = $frame->fd; switch ($data['type']) { case 'auth': // 验证token,绑定用户ID到连接 $userId = authenticate($data['token']); $onlineUsers[$fd] = $userId; // 广播给所有人:用户上线 broadcastOnlineUsers($server, $onlineUsers); break; case 'ping': // 更新心跳时间(可在定时器中检查活跃性) break; } }); $server->on('close', function ($server, $fd) use (&$onlineUsers) { // 连接关闭,移除用户 if (isset($onlineUsers[$fd])) { unset($onlineUsers[$fd]); // 广播给所有人:用户下线 broadcastOnlineUsers($server, $onlineUsers); } }); $server->start();
优点:真正的实时推送,无需轮询;用户体验极佳。
缺点:架构复杂、服务端成本高(常驻内存)、需要额外的运维支持(Nginx代理、进程守护等)。
SSE(Server-Sent Events)+ 数据库/缓存(轻量级实时方案)
原理:服务器单向推送数据给客户端,客户端通过 EventSource 连接一个专门返回在线状态的HTTP端点,服务器定期查询数据库或Redis并推送结果。
适用场景:对实时性要求中等(秒级更新)、不想引入WebSocket复杂度的项目。
实现步骤(PHP端):
-
客户端:
const source = new EventSource('/online-status'); source.onmessage = function(event) { const onlineUsers = JSON.parse(event.data); // 更新UI上的在线状态 }; -
PHP端点(
/online-status):// 这个脚本需要设置无限执行时间 set_time_limit(0); header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); while (true) { // 查询在线用户(从Redis或数据库) $onlineIds = Redis::zrangebyscore("user:online", time()-30, time()); // 以SSE格式输出 echo "data: " . json_encode(['online_ids' => $onlineIds]) . "\n\n"; ob_flush(); flush(); // 每5秒推送一次 sleep(5); }
优点:比WebSocket简单(基于HTTP);浏览器原生支持。
缺点:单向通信,只能服务器推送给客户端;如果不搭配Swoole,PHP的SSE端点会持续占一个PHP-FPM进程(高并发不推荐);连接数有限。
总结与选型建议
| 需求场景 | 推荐方案 | 理由 |
|---|---|---|
| 后台管理、CMS系统 | 方案一(数据库) | 用户少,容错高,实现简单 |
| 高并发的API服务(移动端/Web) | 方案二(Redis缓存 + 心跳) | 性能最优,状态管理灵活,成本可控 |
| 实时聊天、协同编辑、直播 | 方案三(WebSocket) | 必须的实时性,双向通信 |
| 轻量化实时更新(如订单提醒) | 方案四(SSE + Redis) | 适用于单向推送,比WebSocket简单 |
| 不想维护复杂服务 | 方案一 + 方案二混合 | 核心状态存Redis,数据库做后备 |
建议:对于大多数中型PHP项目(如使用 Laravel、Symfony),方案二(Redis 缓存 + 更新时间戳) 是最佳平衡点,你可以结合中间件和定时任务(例如每分钟运行一次,清除超过5分钟未活动的用户)来实现简洁且高效的在线状态管理。