正确做法是用 Redis 原子命令 Cache::inc('pv_total', 1, 3600) 实现 PV 统计,避免并发丢数;UV 统计需用稳定用户标识并优先登录 ID,辅以设备指纹;中间件须前置注册,返回前强制类型转换。

中间件里直接用 cache() 递增 PV 容易丢数
很多人在中间件 handle() 方法里写 cache('pv_total', $val + 1, 3600),看似简单,但高并发下会漏计——因为 PHP 读缓存、加1、写回不是原子操作,两个请求同时读到 100,各自加1再写回,最终只+1而不是+2。
正确做法是交给 Redis 原子命令:Cache::inc('pv_total', 1, 3600)。它底层调用的是 INCR,Redis 保证单 key 自增绝对线程安全。
-
Cache::inc()对不存在的 key 默认初始化为 1,无需先set(0) - 过期时间设为 3600(1 小时)比永不过期更稳妥,避免缓存无限膨胀
- 若需统计单页面 PV,键名建议用
pv_+md5($request->url()),自动忽略 query 参数差异
Cache::inc() 统计 UV 必须配合稳定用户标识
UV 不是“访问人次”,而是“独立用户数”。直接对所有请求都 Cache::inc('uv_total') 等于把 PV 当 UV 用,完全失真。
ThinkPHP 中必须从请求上下文提取稳定 ID:优先取登录态的 $this->auth->id;未登录则 fallback 到设备指纹,例如 sha1($request->header('user-agent') . $request->ip())。
立即学习“PHP免费学习笔记(深入)”;
- 严禁用
session_id()或随机 token,同一用户换浏览器/清缓存就多算一人 - 严禁用纯 IP,NAT 环境下几十人共一个公网 IP 很常见
- 如果项目已用 Redis,更推荐 HyperLogLog:
$redis->pfAdd('uv:' . date('Ymd'), $uid),误差率 ±0.81%,但内存极省
中间件注册顺序错位会导致统计不全
统计中间件必须放在鉴权、日志等中间件之前,否则被拦截的请求(如未登录跳转、404、429)就不会进入统计逻辑。
查看 app/middleware.php,确保你的统计类排在靠前位置,例如:
return [
\app\middleware\RecordUvMiddleware::class,
\app\middleware\RecordPvMiddleware::class,
\think\middleware\CheckRequestCache::class,
\think\middleware\SessionInit::class,
];
- 不要把它放在
SessionInit后面——未登录用户还没初始化 session,$this->auth可能为空 - 不要放在
CheckRequestCache后面——命中缓存的请求根本不会走到你这里 - 测试时可用
curl -I http://your.app/test配合 Redis CLI 直接查 key 是否新增
统计结果返回前端前必须强转类型
Redis 所有数值返回都是字符串,比如 Cache::get('pv_total') 返回的是 "1248" 而非整数 1248。若直接塞给 ECharts 或 JSON API,前端可能渲染失败或图表空白。
后端接口中务必显式转换:
return json([
'pv' => (int) Cache::get('pv_total', 0),
'uv_today' => (int) $redis->pfCount('uv:' . date('Ymd')),
]);
- 别依赖 PHP 自动类型转换,
json_encode()对字符串数字和整数数字输出一致,但某些前端库(如 ECharts 5.4+)会校验 value 类型 - 数据库落库时也要注意:
Db::name('stat')->insert(['pv' => (int)$pv]),防止 MySQL 报 Warning - 最隐蔽的坑:用
Cache::inc()后立刻Cache::get(),两次操作之间若有其他进程也 inc,值可能已变——不要假设“刚 incr 就 get 一定准”



















