在thinkphp 8.0中需基于redis+lua自研token bucket限流:桶容量100、速率20/s,键名含路由与ip;懒加载补令牌、lua原子扣减、支持拒绝/排队/动态降级三策略,并通过ab压测与时间回退验证正确性。

在 ThinkPHP 8.0 项目中实现高并发场景下的平滑限流,需避免简单 sleep 或计数器式硬拦截导致的响应延迟与用户体验断层。Token Bucket 算法能兼顾突发流量容忍度与长期速率可控性,但 TP8.0 官方未内置该算法实现,必须基于 Redis + 时间差懒加载机制自研落地。
初始化令牌桶配置与 Redis 连接
在 app/common/behavior/RateLimitBehavior.php 中新建行为类,构造函数内完成基础参数注入与 Redis 实例获取:
使用 【TP8.0 的 Container::getInstance()->get('cache')->handler()】 获取原生 Redis 实例,不可用 Cache::store('redis') —— 后者封装层会干扰原子操作和 pipeline 执行。
设置桶容量(capacity)为 100,令牌生成速率为每秒 20 个,即 refillIntervalMs = 50ms;桶键名格式为 rate:token:{route}:{ip},确保路由+客户端 IP 双维度隔离。
懒加载式令牌补给逻辑
每次请求到达时才计算应补充的令牌数,不依赖定时任务:
读取桶当前状态:$data = $this->redis->hGetAll($key),其中包含 tokens、last_refill 两个字段;
若 last_refill 为空,说明桶首次使用,直接设 tokens = $this->capacity 并写入 last_refill = time();
否则计算时间差:$deltaMs = (microtime(true) * 1000) - $data['last_refill'],再按速率折算应补令牌:$newTokens = floor($deltaMs / $this->refillIntervalMs);
【注意:必须用 floor 而非 round,避免因浮点误差多补 1 个令牌导致超发】
原子化消费与状态更新
使用 Redis pipeline 批量执行三步操作,防止并发竞争:
第一步:检查当前 tokens 是否 ≥ 1;
第二步:若满足,则执行 HINCRBY $key tokens -1 消耗令牌;
第三步:更新 HSET $key last_refill (microtime(true)*1000);
整个 pipeline 必须包裹在 eval 脚本或 WATCH-MULTI-EXEC 中——但 TP8.0 默认 Redis 驱动不支持 MULTI 嵌套,因此改用 Lua 脚本一次性完成判断+扣减+更新,脚本哈希值缓存复用。
限流响应与降级策略
方法一:直接拒绝
当 Lua 返回 false 时,调用 response()->fail('Too many requests', 429) 并设置 Header:X-RateLimit-Remaining: 0、X-RateLimit-Reset: time()+1;
方法二:排队等待
对非敏感接口(如列表页),启用阻塞模式:循环执行 Lua 脚本,每次失败后 usleep(10000)(10ms),最多重试 3 次,超时则降级为方法一;
方法三:动态降级
当连续 5 次限流触发且 Redis RTT > 20ms,自动切换至本地内存计数器(基于 Swoole Table 或 APCu),避免 Redis 故障引发全站雪崩。
测试验证流程
第一步:启动 ab 压测工具,执行 ab -n 200 -c 50 http://test.local/api/v1/list;
第二步:观察 Redis 中对应 key 的 tokens 字段变化趋势,应呈锯齿状缓慢下降→快速回升→趋近于 capacity;
第三步:检查日志中 429 响应占比,理想值为 max(0, (总请求数 - capacity) / 总请求数),误差超过 ±3% 需校准 refillIntervalMs;
第四步:手动修改系统时间向前拨动 10 秒,验证时间回退时 last_refill 不被覆盖,tokens 不负增长。











