Throttle中间件是ThinkPHP 6最省事的限流方案,基于topthink/think-throttle包,支持IP/控制器/自定义键限流,需正确注册、配置及使用原子操作避免竞态。

直接用 Throttle 中间件最省事
ThinkPHP 6 官方生态里,topthink/think-throttle 是开箱即用的限流方案,不用自己写 Redis 脚本或手撸计数器逻辑。它底层用 Cache 驱动(默认是文件缓存,切到 Redis 只需改配置),支持按 IP、控制器、动作甚至自定义键限流。
常见错误现象:装完包没在 app/middleware.php 里注册中间件类,或者路由里漏了 ->middleware() 调用,结果限流完全不生效。
- 先执行
composer require topthink/think-throttle - 确保
config/throttle.php存在且已发布(若没有,运行php think vendor:publish) - 在路由中明确绑定,比如:
Route::get('api/data', 'Api/Data.index')->middleware(\think\middleware\Throttle::class, ['visit_rate' => '10/m', 'key' => '__IP__']); -
key参数决定限流粒度:'__IP__'是按客户端 IP;'__CONTROLLER__/__ACTION__'是按接口;组合起来如'__IP__/__CONTROLLER__'更精细
visit_rate 的单位和解析规则必须看懂
visit_rate 看似简单,但“5/m”“20/h”“100/d”背后是框架对字符串的硬解析,不是任意表达式。它只认 /m(分钟)、/h(小时)、/d(天)三种后缀,且数值必须是整数。
使用场景:如果你要实现“每 15 秒最多 3 次”,不能写 '3/15s' —— 这会直接报错或退化为不限流。得换算成等效的 /m 单位(即 '12/m'),否则逻辑失效。
- 值为
null表示不限制,别误写成字符串'null' - 时间窗口是硬窗口(非滑动),比如
'10/m'指从第 0 秒开始的每 60 秒重置一次计数,不是“过去 60 秒内累计” - 如果用 Redis 缓存,注意
EXPIRE设置是否准确——某些低版本驱动在set($key, $val, $ttl)时 ttl 传 0 会被忽略,导致 key 永不过期
自定义中间件里用 Cache::inc() 很容易出竞态
很多教程教你在中间件里写 Cache::get($key) → 判断 → Cache::set($key, $count+1),这在并发高时大概率漏判。因为两次缓存操作之间存在时间窗口,多个请求可能同时读到旧值,然后都写入 +1,最终计数少于实际请求数。
性能影响:用 Cache::inc($key) 是原子操作,Redis 或 Memcached 下安全;但如果是文件缓存驱动,inc 本质还是 get+set,照样有竞态。
- 强制用 Redis:在
config/cache.php中把default设为'redis',并确认config/redis.php已正确配置 host/port - 键名建议带业务前缀,比如
'throttle:api:'.md5($request->url()).':'.$request->ip(),避免和其他缓存 key 冲突 - 别依赖
Cache::has()做判断——它和get()是两个独立请求,依然有竞态;应统一用Cache::inc($key)后立刻比对返回值
Redis Lua 脚本才是真可靠,但别乱抄网上片段
真正生产级限流(尤其防刷、秒杀类场景)必须靠 Lua 脚本在 Redis 端完成“读-判-写”原子操作。网上很多 TP6 + Redis 限流示例直接拼接 GET+INCR+EXPIRE,这在 Redis 6.0+ 集群模式下可能因 key 不在同一 slot 而失败。
容易踩的坑:有人把整个限流逻辑写进 Lua,但忘了传参校验,比如没检查 KEYS[2] 是否为数字,导致脚本执行报错,TP6 默认静默吞掉异常,限流形同虚设。
- 推荐用官方
throttle包的 Redis 驱动,它内部已封装好安全 Lua(见vendor/topthink/think-throttle/src/Storage/RedisStorage.php) - 若必须手写,Lua 脚本第一行加
if not tonumber(KEYS[2]) then return 0 end做参数防护 - 键名设计要保证哈希一致性,比如用
throttle:ip:{ip}而不是throttle:{ip}:ip,避免集群分片错乱
限流不是加个中间件就完事——键的设计粒度、缓存驱动的实际行为、时间窗口类型(固定窗 vs 滑动窗),每个点都可能让防御在高并发下当场失效。尤其是 key 字符串里的占位符,一个拼错,整套逻辑就跑偏到别的维度去了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










