最可靠滑动窗口限流方案是thinkphp 6中用redis facade调用zset+lua脚本:通过zremrangebyscore剔除过期项、zcard统计、zadd插入毫秒级时间戳,全程原子执行,避免list方案的并发漏判与精度问题。

直接用 ThinkPHP 6 的 Redis Facade + Lua 脚本实现滑动窗口限流,是目前最可靠、原子性最强的方式。单纯靠 INCR + EXPIRE 是固定窗口,扛不住边界突刺;用 LIST 手动维护时间戳容易在并发下漏判或误判;只有 ZSET + Lua 才能真正实现毫秒级滑动窗口。
为什么必须用 ZSET 而不是 LIST
LIST 方案(如 lPush/rPop)看似简单,但存在两个硬伤:
- 无法精确剔除「窗口外」的请求:比如窗口是 60 秒,但
rPop只能删尾部,而尾部时间戳可能仍在窗口内,导致计数虚高 - 并发时
lLen→lIndex→rPop非原子,两次请求可能读到同一长度、取到同一旧时间戳,结果双双放行
ZSET 天然支持按 score(时间戳)范围删除:ZREMRANGEBYSCORE 一条命令干掉所有过期项,再 ZCARD 统计,全程可封装进 Lua 原子执行。
ThinkPHP 6 中必须写的 Lua 脚本
把限流逻辑写死在 Redis 端,避免网络往返和客户端时钟误差。脚本接收 4 个参数:KEYS[1](限流 key)、ARGV[1](当前毫秒时间戳)、ARGV[2](窗口大小,单位毫秒)、ARGV[3](阈值)。
local window_start = tonumber(ARGV[1]) - tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)
local count = redis.call('ZCARD', KEYS[1])
if count <p>注意三点:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理"><img
src="https://img.php.cn/upload/skill/000/000/081/178960683454849.jpg" alt="Redis Skill - 高性能缓存管理" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="overflowclass">Redis Skill - 高性能缓存管理</a>
<p class="overflowclass">Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill3464" title="Redis Skill - 高性能缓存管理" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
-
ARGV[1] .. ':' .. math.random(...)是为防止相同时间戳导致 value 冲突(ZADD会覆盖),实际可用uniqid()替代 -
PEXPIRE设为window_size + 1000毫秒,确保窗口内最后一条数据过期后,key 还有缓冲期被自动清理 - 不要用
EXPIRE(秒级),必须用PEXPIRE(毫秒级),否则 1 秒内多次请求可能因精度丢失误判
ThinkPHP 6 中间件里调用示例
在 app/middleware/RateLimitMiddleware.php 中写:
use think\facade\Cache;
use think\facade\Log;
<p>public function handle($request, \Closure $next)
{
$ip = $request->ip();
$uri = $request->url(true);
$key = 'rate:sliding:' . md5($ip . '|' . $uri); // 区分 IP+接口</p><pre class="brush:php;toolbar:false;">$nowMs = round(microtime(true) * 1000);
$windowMs = 60 * 1000; // 60秒窗口
$maxReq = 100;
$luaScript = file_get_contents(app()->getAppPath() . 'extra/rate_limit_sliding.lua');
$result = Cache::store('redis')->handler()->eval($luaScript, [$key], [$nowMs, $windowMs, $maxReq]);
if ($result == 0) {
return json(['code' => 429, 'msg' => '请求过于频繁'])->code(429);
}
return $next($request);}
关键点:
-
Cache::store('redis')->handler()是获取原生Redis实例的唯一正路,think\cache\driver\Redis底层就是phpredis - Lua 脚本建议单独存为
app/extra/rate_limit_sliding.lua,别硬编码在 PHP 里,方便复用和调试 -
md5($ip . '|' . $uri)避免 key 名过长或含非法字符;若需更细粒度(如用户 ID),可替换为$request->userId() ?? $ip
上线前必须验证的三个坑
滑动窗口限流最容易在线上翻车的地方就这三个:
- 服务器时间不同步:所有节点必须 NTP 校时,否则
microtime(true)返回值偏差 > 窗口大小,直接失效 - Redis 版本低于 2.6:Lua 支持从 2.6 开始,低版本会报
ERR unknown command `eval` - 未设置
maxmemory-policy:大量限流 key 持续写入,若 Redis 内存满且策略是noeviction,后续ZADD会失败并返回错误,必须设为allkeys-lru或类似策略
滑动窗口的核心不在“怎么写”,而在“怎么让时间戳可信、让 Lua 原子、让 key 生命周期可控”。少踩一个,线上就不会出现 0.02 秒内 200 次请求打穿服务的情况。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










