真正能落地的请求次数统计必须在中间件中用Redis原子计数并缓存兜底:控制器内setInc会导致数据库压垮、重复计数、丢失数据;推荐用Cache::inc()按路由名构造唯一键,设TTL,导出时强转int并防null,历史统计走定时任务落库。

直接在控制器方法里写 $this->model->setInc('count') 会丢数、重复计数、拖慢响应——这不是统计,是埋雷。真正能落地的请求次数统计,必须绕过控制器执行路径,在中间件中完成原子递增,并用缓存兜底。
为什么不能在控制器里手动调用 setInc
三个硬伤无法回避:
-
setInc是数据库层面的原子操作,但每次调用都触发一次 SQL 写入,QPS 超过 200 就容易压垮 MySQL 连接池 - 没做去重逻辑,F5 刷新、前端轮询、浏览器预加载都会被算作新请求,
UV和PV完全混淆 - 控制器执行完才写,若中间件抛异常或响应中断(如超时、重定向),计数就丢失,且无法回溯
推荐方案:中间件 + Redis 原子计数(ThinkPHP 6/8)
把计数逻辑下沉到全局中间件,用 Cache::inc() 替代任何数据库写入,键名按「控制器+方法」标准化构造:
- 取路由名而非 URL:
$route = $request->rule()->getName() ?: $request->controller() . '/' . $request->action() - 构造唯一键:
'api:call:' . strtolower(str_replace('\', ':', $route)),例如api:call:app:controller:user:login - 递增并设 TTL:
Cache::inc($key, 1, 3600),1 小时后自动过期,避免 key 污染 - 首次调用时
Cache::inc()会自动初始化为 1,无需先Cache::set($key, 0)
导出统计结果时必须处理的数据类型陷阱
Redis 返回的计数值永远是字符串,而前端图表(如 ECharts)要求纯数字数组:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
立即学习“PHP免费学习笔记(深入)”;
- 后端接口返回前务必强制转换:
'count' => (int) Cache::get($key) - 不要依赖 PHP 自动类型转换,某些版本下
(int) '17'可能变成0(比如开头有不可见字符) - 前端接收 JSON 后仍需再转一次:
data.map(d => Number(d.count)),双保险 - 查不到值时
Cache::get()返回null,要默认给0:(int) (Cache::get($key) ?: 0)
想查历史累计次数?别扫 keys,改用定时落库
Redis 的 KEYS api:call:* 在生产环境会阻塞主线程,绝对禁止用于接口响应中:
- 另起命令行任务:
php think stat:sync,每小时执行一次 - 用
Cache::store('redis')->getRedis()->keys('api:call:*')扫描(仅限后台任务,非 Web 请求) - 批量读取:
array_map(fn($k) => [$k, Cache::get($k)], $keys) - 写入数据库后立即
Cache::delete($k)清空,避免重复统计
真正难的不是写几行 Cache::inc(),而是让每个键的生命周期、聚合口径、数据类型都对齐业务场景——比如按小时滚动统计就不能把日期写进 key,而按日 UV 统计就必须带 uv:20260817 后缀。键设计错了,后面所有分析都是错的。


















