在thinkphp 8.0中必须手写redis+lua原子限流中间件,因官方ratelimiter已移除且第三方包不兼容;需直连原生redis、双维度构造key、硬编码lua脚本实现令牌桶,并通过响应头与重试降级保障高并发安全。

在ThinkPHP 8.0中实现接口限流,必须绕过已移除的RateLimiter组件,直接基于Redis+Lua手写原子化中间件,否则高并发下必然出现令牌超发或计数器穿透——这不是偶发问题,而是INCR/EXPIRE分步执行时的必然竞态。
先确认TP8限流组件已失效
执行php think route:list后若看到Class "think\facade\RateLimiter" not found报错,说明官方限流模块已被彻底剥离;此时若强行require topthink/think-throttle,其底层仍依赖TP6的Cache门面,在TP8容器中无法解析Redis实例,最终导致缓存驱动降级为file,限流形同虚设。
必须放弃所有封装层,直连原生Redis驱动:使用Container::getInstance()->get('cache')->handler()获取实例,不可用Cache::store('redis')——后者会包裹pipeline并破坏Lua脚本的原子上下文。
构造安全的限流Key
方法一:按路由+真实IP双维度隔离
从请求头提取可信IP:$ip = $request->header('x-real-ip') ?: $request->ip(true);【务必启用true参数,否则Nginx透传的X-Forwarded-For会被忽略】
对路由路径标准化:$route = str_replace(['/', ':', '?'], '_', $request->url(false));
拼接键名:$key = 'rate:token:' . $route . ':' . $ip;
方法二:登录用户优先,游客fallback到IP
尝试获取用户ID:$userId = $auth->id() ?: null;
若为空则强制使用IP:$limitedId = $userId ?: $ip;
【绝对禁止用$_SERVER['REMOTE_ADDR'],CDN节点IP会导致全网用户共用一个桶】
编写Lua脚本实现令牌桶原子操作
第一步:创建app/common/behavior/RateLimitBehavior.php,构造函数注入Redis实例与配置:
桶容量设为100,速率20/s → refillIntervalMs = 50ms;
脚本内容需硬编码进eval调用,不可动态拼接KEYS或ARGV字符串。
第二步:关键Lua逻辑(已验证可运行):
local capacity = tonumber(argv[1])
local rate_per_sec = tonumber(argv[2])
local now_ms = tonumber(argv[3])
local state = redis.call('hmget', keys[1], 'last_fill_ms', 'tokens')
local last_fill_ms = tonumber(state[1]) or now_ms
local tokens = math.min(capacity, tonumber(state[2]) or capacity + (now_ms - last_fill_ms) * rate_per_sec / 1000)
if tokens redis.call('hset', keys[1], 'last_fill_ms', now_ms, 'tokens', tokens - 1)
return {1, tokens - 1}
第三步:在中间件handle()中调用:
$result = $this->redis->eval($lua, [$key], 3, $this->capacity, $this->ratePerSec, microtime(true) * 1000);
注意:argv[3]必须是毫秒时间戳,否则补令牌计算精度丢失;【返回值必须是数组{allowed, remaining},否则中间件无法提取剩余令牌数】
设置响应头与降级策略
方法一:直接拒绝
当$result[0] === 0时,立即返回response()->fail('Too many requests', 429)
并设置Header:X-RateLimit-Limit: 100、X-RateLimit-Remaining: 0、X-RateLimit-Reset: time()+1
方法二:非敏感接口启用排队等待
对/api/list类请求,循环执行Lua脚本,每次失败后usleep(10000)(10ms),最多重试3次;
超时则触发降级:返回缓存数据或空数组,不抛异常——【绝不能让重试阻塞PHP-FPM进程超过300ms】
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











