ThinkPHP 6 的 Cache::inc() 总返回 false 是因默认 File/Array 驱动不支持原子自增,需改用 Redis/Memcached;同时要注意 key 构造、限流中间件响应规范及分布式兜底逻辑。

ThinkPHP 6 的 Cache::inc() 为什么总返回 false?
因为默认缓存驱动不支持原子自增——File 和 Array 驱动根本没实现 inc() 方法,调用直接返回 false。必须换用支持原子操作的驱动,比如 Redis 或 Memcached。
实操建议:
- 检查当前缓存配置:
config/cache.php中'default' => 'redis',且'redis'配置项已正确填写 host/port - 验证驱动是否生效:运行
Cache::store('redis')->inc('test_key', 1),若返回整数说明可用;返回false则可能是 Redis 连接失败或权限问题 - 别在开发环境用
File驱动测试限流逻辑——它无法保证并发安全,本地测通了上线必出问题
用 Cache::remember() 实现时间窗口计数器的坑
想用「缓存存在则读、不存在则设初值并设过期」来实现滑动窗口?小心 remember() 的竞态:两个请求同时发现 key 不存在,都会执行回调,导致计数虚高。
正确做法是绕过 remember(),改用原子操作 + 显式过期控制:
立即学习“PHP免费学习笔记(深入)”;
- 先用
Cache::store('redis')->inc($key)自增(首次会自动初始化为 0 再 +1) - 紧接着用
Cache::store('redis')->set($key, Cache::store('redis')->get($key), $ttl)补过期时间(注意:不能依赖inc()自带过期,Redis 的INCR不带 TTL) - 更稳妥的是组合命令:
eval脚本或使用Cache::store('redis')->handler()->eval(...)执行 Lua 原子脚本
示例(Lua 原子写法):
Cache::store('redis')->handler()->eval(
"if redis.call('exists', KEYS[1]) == 0 then redis.call('setex', KEYS[1], ARGV[1], 1) else redis.call('incr', KEYS[1]) end",
1,
$key,
$ttl
);
IP + 接口路径拼接作缓存 key 时的边界情况
直接拼 $ip . '_' . $request->url() 很危险:URL 含查询参数顺序不定、有签名参数、带临时 token,会导致同一请求生成多个 key,限流失效。
应标准化 key 构造逻辑:
- 只取路由标识,不用完整 URL:
$request->rule()->getName()或手动定义接口唯一标识如'api_login' - IP 要做归一化:
$request->ip(0, true)防止代理头伪造;IPv6 地址需转为固定格式(如去掉冒号、补零),避免同一 IP 多个 key - 如果需区分用户级限流,别直接存 token 或 session_id——它们可能未建立,优先用
$request->header('authorization')提取 Bearer token 前缀哈希(如md5(substr($token, 0, 32)))
限流后返回 429 但前端收不到响应头?
ThinkPHP 默认限流中间件(如 thans/think-captcha 或自定义)若用 halt() 或 exit 终止流程,Response 对象未被框架统一发送,Header 就丢了。
必须走标准响应流程:
- 不要
die('too many'),改用return response('', 429)->header(['X-RateLimit-Remaining' => '0']) - 确保中间件 return 的是
Response实例,不是字符串或数组(TP6 对返回值类型敏感) - 检查是否被后续中间件覆盖:比如 CORS 中间件在限流之后执行,又把状态码改回 200 —— 把限流中间件注册顺序调到最前
真正麻烦的是分布式场景下 Redis 连接抖动:Cache::inc() 抛异常时,你得决定是放行(风险)还是阻断(影响可用性)。这个兜底逻辑,没人替你写。



















