HyperLogLog是ThinkPHP 8.0中百万级UV日统计的唯一可行方案,固定12KB内存支持亿级去重,需规范键名(uv:Ymd)、稳定用户标识、自动过期及容忍±0.81%误差。

在 ThinkPHP 8.0 项目中,需要按天统计网站或接口的独立访客(UV),且日均访问量达百万级,必须避免用 Set 存用户 ID 导致 Redis 内存暴涨——HyperLogLog 是唯一能以固定 12KB 内存支撑亿级去重计数的方案。
确认 Redis 连接与 HyperLogLog 支持
进入 config/cache.php,检查 'default' => 'redis' 是否已启用;若未配置 Redis,请先在 config/database.php 中补全 Redis 主机、端口、密码及数据库编号。ThinkPHP 8.0 默认封装了 Redis::pfAdd() 和 Redis::pfCount() 方法,无需额外引入扩展。
运行 php think run 启动内置服务器后,在任意控制器中执行:$this->cache->store('redis')->getRedis()->pfadd('test:hll', 'test');,无报错即表示支持正常。
定义 UV 统计键名规范
键名必须带日期后缀,格式为 【uv:Ymd】(如 uv:20260708),绝不可使用 uv:all 或 uv:home 这类长期复用 key——否则数据永远累积,PFCOUNT 结果会持续膨胀且无法区分日期。
立即学习“PHP免费学习笔记(深入)”;
用户标识统一用登录态下的 $user->id;未登录场景则 fallback 到设备指纹(如 sha1($_SERVER['HTTP_USER_AGENT'].$_SERVER['REMOTE_ADDR'])),严禁使用 session_id 或随机 token,否则同一用户多次刷新被计为多人。
在请求入口自动写入 UV
推荐在全局中间件中注入统计逻辑,例如新建 app/middleware/RecordUvMiddleware.php:
第一步:获取当前日期字符串 $date = date('Ymd');
第二步:构造键名 $key = "uv:{$date}";
第三步:从请求上下文提取稳定用户标识,优先取 $this->auth->id ?? $device_id;若两者皆空,跳过写入(不统计非法请求)
第四步:调用 $this->cache->store('redis')->getRedis()->pfAdd($key, $uid); ——这一步是幂等的,重复写入同一 uid 不影响计数结果
第五步:为该 key 设置 30 天过期,防止历史 key 持续堆积:$this->cache->store('redis')->getRedis()->expire($key, 86400 * 30);
查询当日 UV 数值
方法一:直接调用 Redis 命令
$uv = $this->cache->store('redis')->getRedis()->pfCount("uv:" . date('Ymd'));
方法二:封装成服务类(推荐)
在 app/service/StatService.php 中定义:
public static function getTodayUv(): int { return cache('redis')->getRedis()->pfCount('uv:' . date('Ymd')) ?: 0; }
注意:PFCOUNT 返回的是概率估算值,误差率约 ±0.81%,当单日 UV 小于 100 时误差可能达 ±10,因此【不适用于精确要求个位数准确的场景】,仅用于后台看板、运营报表等容忍近似值的用途。
合并多日 UV(如周 UV、月 UV)
使用 PFMERGE 命令一次性合并多个日期的 HyperLogLog:
$redis = $this->cache->store('redis')->getRedis();
$keys = ['uv:20260701', 'uv:20260702', 'uv:20260703'];
$redis->pfMerge('uv:week:202607', ...$keys);
$weeklyUv = $redis->pfCount('uv:week:202607');
合并后的 key 也需手动设置过期时间,否则永久存在:$redis->expire('uv:week:202607', 86400 * 7);



















