Redis 滑动窗口 vs 固定窗口对比
解读
在国内高并发业务(秒杀、限流、防刷、API 频次控制)面试中,面试官用这道题同时考察三件事:
- 对「窗口」这一限流模型的本质理解;
- 对 Redis 原子性、内存结构、Lua 脚本、过期策略的实战掌握;
- 能否结合 PHP-FPM 或 Swoole 协程场景给出可落地的代码与性能数据。
回答时必须体现「业务精度」「Redis 精度」「PHP 精度」三个维度,否则会被追问「你这套方案双十一能扛 20w 并发吗?」
知识点
-
固定窗口(Fixed Window)
- 以自然时间为边界(如每分钟的 00–59 秒),Redis 用
INCR单键 +EXPIRE实现; - 临界突变问题:59 秒来 100 请求,01 秒又来 100 请求,实际 2 秒打 200,超限 2 倍;
- 内存 O(1),CPU O(1),写操作简单,可水平扩展,但精度最低。
- 以自然时间为边界(如每分钟的 00–59 秒),Redis 用
-
滑动窗口(Sliding Window)
- 以「当前时间点往前回溯 N 秒」为动态区间,Redis 用「时间戳 Score + 唯一请求 ID」的 ZSet 实现;
- 每次先
ZREMRANGEBYSCORE清理过期时间戳,再ZCARD计数,最后ZADD当前请求; - 无临界突变,精度可到毫秒级,内存 O(请求量),CPU O(logN);
- 需 Lua 脚本保证「清理-计数-写入」原子性,否则并发下会超卖。
-
国内业务选型经验
- 固定窗口 + 小窗口(≤1s)+ 令牌桶补偿,可cover 90% 的 C 端秒杀场景;
- 滑动窗口用于「单 IP 每分钟只能发 5 条短信」类强合规需求,内存上限通过「ZSet 长度硬截断」或「Redis Stream 裁剪」兜底;
- 大厂线上常把滑动窗口降级为「多桶滑动」:1s、10s、1min 三档 ZSet,减少单 Key 热写。
-
PHP 侧落地细节
- PHP-FPM 短连接下,用
predis的eval()一次性把 Lua 脚本发到 Redis,避免多次 round-trip; - Swoole 协程环境用
mix/redis连接池,Lua 脚本哈希缓存到SCRIPT LOAD,性能可再提 25%; - 内存估算:滑动窗口单 Key 在 1w 并发/分钟、ID 长度 36 字节场景,约占用 1.2 MB,需提前设置
max-ziplist-value防止编码降级。
- PHP-FPM 短连接下,用
答案
固定窗口
实现:以「分钟」为 Key,INCR 后 EXPIRE 60。
优点:代码极简、单命令原子、内存恒定。
缺点:边界双倍突刺,精度秒级,不适合合规类限流。
PHP 代码:
$key = 'api_limit:' . date('YmdHi');
$max = 100;
$current = $redis->incr($key);
if ($current === 1) {
$redis->expire($key, 60);
}
if ($current > $max) {
http_response_code(429); exit('请求超限');
}
滑动窗口 实现:ZSet 存时间戳 Score,Lua 脚本保证原子。 优点:无突刺,精度可到毫秒,满足合规。 缺点:内存随请求线性增长,热 Key 大对象可能触发 Redis 延迟。 PHP + Lua 代码:
$lua = <<<'LUA'
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local uuid = ARGV[4]
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local cnt = redis.call('ZCARD', key)
if cnt >= limit then
return 0
end
redis.call('ZADD', key, now, uuid)
redis.call('EXPIRE', key, window)
return 1
LUA;
$now = microtime(true) * 1000;
$uuid = uniqid('', true);
$passed = $redis->eval($lua, 1, 'sliding:api', 60000, 100, $now, $uuid);
if (!$passed) {
http_response_code(429); exit('请求超限');
}
一句话总结: 固定窗口拿「简单+性能」换「精度」,滑动窗口拿「内存+复杂度」换「精度」;国内面试答到「边界突刺」「ZSet 热 Key」「Lua 原子」这三点,即可拿到高分。
拓展思考
- 冷热 Key 分离:滑动窗口在 618 大促会成为热点,可用
HASH_TAG把 Key 按{limit}:ip:尾号拆成 10 个分片,降低单 Redis 槽压力。 - 多维度组合:把「用户ID + 接口 + 自然分钟」做成固定窗口,「IP + 滑动 10 秒」做成滑动窗口,两层漏斗,兼顾性能与防刷。
- Redis 7 的
FUNCTION:把限流脚本注册为持久化函数,解决老集群SCRIPT FLUSH后重新加载的抖动问题,适合金融支付场景。 - 与 PHP 业务线程模型结合:在 Swoole 4.8+ 中,用
Channel把拒绝请求异步上报到监控系统,避免限流逻辑阻塞业务协程。 - 成本对比:同等 20w QPS,固定窗口只需要 3 个 4G Redis 主从,滑动窗口需要 5 个 8G 节点并开启
list-compress-depth压缩,年费用相差约 3 万元,面试时可主动抛出「成本意识」加分。