Redis 滑动窗口 vs 固定窗口对比

解读

在国内高并发业务(秒杀、限流、防刷、API 频次控制)面试中,面试官用这道题同时考察三件事:

  1. 对「窗口」这一限流模型的本质理解;
  2. 对 Redis 原子性、内存结构、Lua 脚本、过期策略的实战掌握;
  3. 能否结合 PHP-FPM 或 Swoole 协程场景给出可落地的代码与性能数据。

回答时必须体现「业务精度」「Redis 精度」「PHP 精度」三个维度,否则会被追问「你这套方案双十一能扛 20w 并发吗?」

知识点

  1. 固定窗口(Fixed Window)

    • 以自然时间为边界(如每分钟的 00–59 秒),Redis 用 INCR 单键 + EXPIRE 实现;
    • 临界突变问题:59 秒来 100 请求,01 秒又来 100 请求,实际 2 秒打 200,超限 2 倍;
    • 内存 O(1),CPU O(1),写操作简单,可水平扩展,但精度最低。
  2. 滑动窗口(Sliding Window)

    • 以「当前时间点往前回溯 N 秒」为动态区间,Redis 用「时间戳 Score + 唯一请求 ID」的 ZSet 实现;
    • 每次先 ZREMRANGEBYSCORE 清理过期时间戳,再 ZCARD 计数,最后 ZADD 当前请求;
    • 无临界突变,精度可到毫秒级,内存 O(请求量),CPU O(logN);
    • 需 Lua 脚本保证「清理-计数-写入」原子性,否则并发下会超卖。
  3. 国内业务选型经验

    • 固定窗口 + 小窗口(≤1s)+ 令牌桶补偿,可cover 90% 的 C 端秒杀场景;
    • 滑动窗口用于「单 IP 每分钟只能发 5 条短信」类强合规需求,内存上限通过「ZSet 长度硬截断」或「Redis Stream 裁剪」兜底;
    • 大厂线上常把滑动窗口降级为「多桶滑动」:1s、10s、1min 三档 ZSet,减少单 Key 热写。
  4. PHP 侧落地细节

    • PHP-FPM 短连接下,用 prediseval() 一次性把 Lua 脚本发到 Redis,避免多次 round-trip;
    • Swoole 协程环境用 mix/redis 连接池,Lua 脚本哈希缓存到 SCRIPT LOAD,性能可再提 25%;
    • 内存估算:滑动窗口单 Key 在 1w 并发/分钟、ID 长度 36 字节场景,约占用 1.2 MB,需提前设置 max-ziplist-value 防止编码降级。

答案

固定窗口 实现:以「分钟」为 Key,INCREXPIRE 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 原子」这三点,即可拿到高分。

拓展思考

  1. 冷热 Key 分离:滑动窗口在 618 大促会成为热点,可用 HASH_TAG 把 Key 按 {limit}:ip:尾号 拆成 10 个分片,降低单 Redis 槽压力。
  2. 多维度组合:把「用户ID + 接口 + 自然分钟」做成固定窗口,「IP + 滑动 10 秒」做成滑动窗口,两层漏斗,兼顾性能与防刷。
  3. Redis 7 的 FUNCTION:把限流脚本注册为持久化函数,解决老集群 SCRIPT FLUSH 后重新加载的抖动问题,适合金融支付场景。
  4. 与 PHP 业务线程模型结合:在 Swoole 4.8+ 中,用 Channel 把拒绝请求异步上报到监控系统,避免限流逻辑阻塞业务协程。
  5. 成本对比:同等 20w QPS,固定窗口只需要 3 个 4G Redis 主从,滑动窗口需要 5 个 8G 节点并开启 list-compress-depth 压缩,年费用相差约 3 万元,面试时可主动抛出「成本意识」加分。