ThinkPHP 8.0 接口限流需用 Redis + Lua 脚本在中间件实现原子化令牌桶算法,Key 需维度隔离并转义特殊字符,响应须返回 429 状态码及标准限流头部。

ThinkPHP 8.0 实现接口限流,核心是绕过已移除的 RateLimiter 组件,用 Redis + Lua 脚本在中间件层自研原子化限流逻辑。不能依赖内存计数、INCR/EXPIRE 组合或第三方未适配包,否则高并发下必出现超发或竞态。
必须用 Lua 脚本保证原子性
Redis 单命令无法完成“读状态→判断→扣减→更新时间戳”整套操作。INCR+EXPIRE 分两步会漏放;WATCH-MULTI-EXEC 在 TP8 默认 Redis 驱动中不支持嵌套。唯一可靠方式是封装 Lua 脚本,通过 Redis::eval() 一次性执行:
- 脚本内显式
tonumber()转换参数,避免字符串比较出错 - 返回数组格式,如
return {allowed, remaining},便于中间件解析 - 使用
KEYS[1]引用键名,兼容 Redis Cluster 分片
中间件中构造带维度隔离的 Key
Key 必须体现限流粒度,且规避非法字符导致驱动解析失败:
- 不要直接拼接
$request->url() . $request->ip(),斜杠和冒号可能引发 predis 等驱动异常 - 统一替换特殊字符:
str_replace(['/', ':', '?'], '_', 'rate:api_v1_user_info_' . ($userId ?: $request->ip())) - 游客请求必须 fallback 到 IP,不能留空,否则所有未登录用户共用一个桶
推荐使用令牌桶算法(非简单计数)
固定窗口易被临界突刺击穿,令牌桶更适配真实业务流量:
立即学习“PHP免费学习笔记(深入)”;
- 桶容量设为 100,速率 20/s(即每 50ms 补 1 个)
- 每次请求懒加载补令牌:读
last_refill,算毫秒差,floor($deltaMs / 50)得应补数 - 扣减时用 pipeline 或 Lua 一次性完成判断与写入,防止并发竞争
响应要符合标准协议头
限流拦截不是简单 return fail,需返回语义化信息:
- 状态码必须为
429 Too Many Requests - 设置
X-RateLimit-Limit: 100、X-RateLimit-Remaining: 32、X-RateLimit-Reset: 1726957020 - 对排队策略,可加
Retry-After: 1,前端据此决定是否重试
不复杂但容易忽略



















