直接查 session 表统计在线人数不可靠,因 session 过期滞后、高并发读写锁导致延迟;应使用 redis set 存储用户 id 并设 5 分钟过期,每次请求 sadd + expire 整个 key,用 scard 统计数量,避免 smembers 全量拉取。

为什么 session 表查在线人数不准
直接查 session 数据库表,看起来最简单,但实际几乎不可靠。Laravel 默认 session 过期时间是 120 分钟,而用户关闭浏览器、网络中断、页面挂起后没发心跳,session 记录还在,只是人早就不在了。更麻烦的是,如果用了文件或数据库驱动,高并发下读写锁、事务延迟会让统计严重滞后甚至卡住请求。
实操建议:
- 别依赖
session表的last_activity字段做实时判断,它只反映“最后一次写入时间”,不是“当前活跃” - 如果非要用数据库方案,至少加一个
updated_at字段并配合定时任务清理(比如每分钟删掉 5 分钟无更新的记录),但依然有延迟 - 真正要准,必须主动维护“心跳”状态,而 Redis 是唯一靠谱的选择
Redis 存什么:用 SET 还是 HASH?
存在线用户标识,核心是“去重 + 可过期 + 可批量查”。SET 天然去重,EXPIRE 支持单 key 过期,SCARD 直接返回数量——这三个能力刚好闭环。用 HASH 或 LIST 反而多出管理成本,还容易因重复 HSET 导致误增。
实操建议:
- 用
SET类型,key 命名为online_users:202405(带日期前缀便于归档) - 每个用户存一个唯一标识,推荐
auth()->id();未登录用户可用request()->ip() . '_' . request()->userAgent()(注意长度限制) - 每次请求时执行:
redis()->sAdd('online_users:202405', $userId)+redis()->expire('online_users:202405', 300)(5 分钟过期) - 不要在中间件里用
pipeline批量操作,避免某次失败导致整个 pipeline 中断,单条命令更稳
怎么避免重复计数和漏计数
关键不在“存”,而在“什么时候存、什么时候删”。Laravel 没有全局 exit 钩子,不能靠响应结束自动清理;也不能全靠前端心跳,因为移动端切后台、浏览器休眠会导致心跳中断。
实操建议:
- 只在登录态已确认、且用户大概率在看页面的时机写入:比如首页、仪表盘、聊天页等路由对应的控制器方法开头
- 不依赖前端定时器上报,改用「服务端被动刷新」:每次用户发起有效请求(非图片/CSS/JS),就刷新该用户的 set 成员过期时间——但 Redis 的
SETEX不支持对 set 中单个成员设过期,所以得换思路 - 正确做法:把用户 ID 当作 key,用
SET存在线状态,过期时间设在 value 上(即用SET user:123 online EX 300),再用KEYS user:*配合COUNT统计——但 KEYS 性能差,线上禁用 - 最终推荐:坚持用
online_users这个 set,每次请求都sAdd+expire整个 set。虽然看起来浪费,但 Redis expire 是惰性+定期双策略,实际开销极低,且逻辑最简、最不容易出错
性能和边界情况要注意什么
当在线人数上万,SCARD 本身仍是 O(1),没问题;但如果你同时想查“哪些人在”,SMEMBERS 就会把全部 ID 拉到 PHP 内存,可能撑爆内存或超时。
实操建议:
- 只用
SCARD做数量统计,别用SMEMBERS查列表,真要查就分页用SSCAN - Redis 默认最大内存 512MB,一万个字符串 ID 占不到 1MB,但如果你把完整用户信息(JSON)塞进去,就很容易爆;只存 ID 或短 token
- 集群环境下,确保所有 Web 节点连的是同一个 Redis 实例或同一分片,否则统计分散
- 如果用了 Horizon 或队列任务,注意它们不会触发在线统计逻辑——这是对的,队列本就不代表“用户在线”
真正难的不是代码怎么写,是怎么定义“在线”:是 30 秒内有请求?还是正在 WebSocket 连接中?不同业务场景阈值不同,这个判断逻辑一旦定错,后面所有优化都是白搭。










