Redis INCR 实现原子计数,键名按接口路径或用户维度设计,TTL 控制生命周期;定时任务每小时扫描 key 批量落库,避免请求中直写 MySQL;中间件需标准化路由规则防重复计数;数据库表精简字段,写入走异步队列。

用 Redis INCR 做原子计数,不碰数据库
直接写数据库统计接口调用量,在高并发下会迅速拖垮 MySQL,尤其是 INSERT 频繁时连接池打满、锁表、慢查询飙升。Redis 的 INCR 是单线程原子操作,毫秒级响应,天然适合计数场景。
关键点在于键名设计和过期控制:
-
api:count:/v1/user/info—— 按接口路径聚合,简单直接 -
api:count:user_{$uid}:/v1/order/list—— 加用户维度,需确保$uid可靠(如从 JWT 或登录态取,不用 session_id) - 不要在 key 里拼时间戳(如
/v1/user/info:20260817),而是靠 TTL 控制生命周期:Cache::inc($key, 1, 3600)表示 1 小时后自动过期 - 避免用
Cache::get+Cache::set手动递增——非原子,高并发下会丢计数
定时落库用命令行任务,别在请求中 flush
Redis 计数只是中间态,审计或报表需要落库。但绝不能在每次请求里执行 Db::name('api_call_log')->insert(),否则等于把压力又引回数据库。
正确做法是分离读写:Redis 负责实时计数,后台定时任务负责批量搬运。
立即学习“PHP免费学习笔记(深入)”;
- 写一个命令行任务:
php think api:sync-count,每小时执行一次 - 任务内用
Cache::store('redis')->handler()->keys('api:count:*')扫描所有计数 key - 对每个 key 执行
Cache::store('redis')->handler()->get($key)读值,再批量写入数据库:Db::name('api_call_log_daily')->insertAll($batch) - 最后用
Cache::store('redis')->handler()->del(...)清空已同步的 key
注意:不要用 KEYS * 在生产环境扫全量 key,改用 SCAN 游标遍历,避免阻塞 Redis 主线程。
中间件里做计数,但要避开路由分组陷阱
很多人把计数逻辑放在全局中间件,结果发现 /api/v1/user/1 和 /api/v1/user/2 被算成同一个接口——因为没做路径标准化。
ThinkPHP 的 $request->url() 返回带参数的完整 URL,$request->pathinfo() 是路由解析后的路径(推荐),但 RESTful 路由可能仍含变量段。
- 用
Route::getRule($request->rule())获取当前匹配的原始路由规则,比如'api/v1/user/:id',比直接取$request->pathinfo()更准确 - 若用
think-throttle类似中间件,注意它默认用__IP__或__ROUTE__作 key 前缀,需确认其getLimitKey()方法是否已适配你的路由定义方式 - 调试时加日志:
Log::info('count key', ['key' => $key, 'path' => $request->pathinfo(), 'rule' => $request->rule()]),避免 key 冲突或重复计数
数据库表结构要精简,写入走异步队列
即使你决定落库,也别让 api_call_log 成为性能瓶颈。这张表只存必要字段,且插入必须异步。
- 表结构建议最小化:
id(自增主键)、api_path(VARCHAR 255,索引)、created_at(DATETIME,不存微秒)、ip(VARCHAR 45,可选)——去掉 user_id、ua、refer 等非核心字段 - 绝不直接
Db::insert(),改用think-queue推送:Queue::push('app\job\ApiCallLogJob', ['path' => $path, 'ip' => $ip]) - 消费者进程用
--max-jobs=100批量处理,每批组装insertAll,减少 IO 次数 - 如果连队列都扛不住(比如峰值 5k QPS),那就只保留 Redis 计数 + 定时落库,放弃实时日志写入
最易被忽略的一点:Redis 计数 key 的 TTL 必须大于定时任务周期。比如任务每小时跑一次,TTL 却只设 1800 秒(30 分钟),就会漏掉跨周期的请求。设为 3600 或更长,并在任务里加兜底逻辑(如扫描 2 小时内的 key)。



















