TP8已彻底移除内置RateLimiter和throttle中间件,类名不存在导致直接报错;必须手写Lua+Redis原子脚本实现限流,避免INCR/EXPIRE竞态超发,并正确构造key、获取真实IP、返回带Retry-After的429响应及配置Redis连接池。

TP8 里不能直接用 throttle 中间件,内置限流组件已移除,必须手写中间件 + Lua 脚本 + Redis 才能可靠限流。
为什么 TP8 的 throttle 中间件根本不起作用
不是配置错,是它压根不存在了。TP8 已彻底移除 think\facade\RateLimiter 和配套的 throttle 中间件,连类名都找不到——你写 ->middleware('throttle:60,1') 会直接报 Class "think\facade\RateLimiter" not found,请求中断,不会 fallback 到内存或静默跳过。
常见误操作包括:
- 照搬 TP6 文档,在路由里加
throttle,结果 500 报错 - 改用第三方扩展包,但多数未适配 TP8 的容器绑定和
Request生命周期,$request->ip()取不到真实 IP 或中间件不触发 - 试图用
Cache::inc()+Cache::set()模拟,高并发下因非原子操作导致超发(比如 100 个请求同时读到剩余 1,全部通过判断后 DECR)
必须用 Lua 脚本调用 Redis,否则必漏放
INCR/EXPIRE 分两步执行,在并发场景下必然出现竞态:多个请求同时读取计数、同时判断通过、同时扣减,结果远超阈值。这不是“概率低”,而是压测时 100% 复现。
立即学习“PHP免费学习笔记(深入)”;
TP8 提供 Redis::eval(),可安全执行原子脚本。关键点:
-
argv全是字符串,tonumber(argv[1])必须显式转换,否则if remaining > 0永远为 false - 返回值必须是数组,例如
return {allowed, remaining},中间件才能拿到剩余令牌数用于调试或响应头 - key 构造要带业务维度,比如
rate_limit:ip:$request->header('X-Real-IP', $request->ip()):path:$request->path(),避免所有接口共用一个计数器
如何在中间件中正确获取客户端真实 IP
别信 $request->ip(),Nginx 后面默认返回 127.0.0.1;也别无脑用 X-Forwarded-For,它可被伪造。
正确做法是信任反向代理添加的可信头:
- Nginx 配置里固定写死:
proxy_set_header X-Real-IP $remote_addr; - PHP 中只取:
$request->header('X-Real-IP', $request->ip()) - 如果必须用
X-Forwarded-For,得在config/app.php设置'trusted_proxies' => ['127.0.0.1'],否则框架不解析该头
测试时用 curl -H "X-Real-IP: 192.168.1.100" http://your.app/api/test 最可控,比模拟真实链路更准。
自定义中间件里怎么返回标准 429 响应
TP8 默认没封装 429 响应逻辑,你要自己构造 JSON 并塞 Retry-After 头。
示例关键片段:
if (!$allowed) {
$response = response()->json([
'error' => 'Too many requests',
'retry_after' => $remainingTTL ?? 60
], 429);
$response->header('Retry-After', $remainingTTL ?? 60);
return $response;
}
注意:$remainingTTL 来自 Lua 脚本返回的第二项(即 key 的剩余存活秒数),不是硬编码。漏掉这个,前端无法做倒计时重试。
最易被忽略的是 Redis 连接池配置——TP8 默认没开连接池,高并发下 Redis 连接耗尽,限流中间件反而成性能瓶颈。必须在 config/redis.php 中启用 'pool' => ['max_idle_time' => 60],并确认缓存驱动已切到 redis,别用 file。



















