第 0 号窗口 · 0.0–59.9s
rate:product:42:0
收到 5 次,放行 5 次,计数值 5 / 5
Redis / Fixed Window / Incident Lab
固定窗口只认识“这个 key 已经计了几次”,它不认识“最近 60 秒”。把两批请求压在窗口切换前后,看看两个都合规的计数器如何拼出一次突刺。
操作台 · 选择批次后点击时间轴
默认相差 0.2 秒。时间轴的正中间,就是 Redis 从一个计数 key 切到下一个 key 的时刻。
精确操作:先选 A 批或 B 批,再点击时间轴;时间轴获得焦点后,可用 ← / → 每次移动 0.1 秒。
A 批在 59.9 秒耗尽第 0 号窗口的 5 个名额;0.2 秒后,B 批进入第 1 号窗口,又拿到完整 5 个名额。两个窗口都没有超配额,但接口在 0.2 秒内放行了 10 次。
Redis 眼中的现场
rate:product:42:0
rate:product:42:1
事故复盘
产品经理说的“每 60 秒最多 5 次”,通常指任意连续 60 秒都不能超过 5 次。但固定窗口实现的是:自然切开的每个 60 秒区间,各自最多 5 次。两句话只差一点点,流量一到边界,结果可以差一倍。
在商品详情接口里,这种误差可能只是让热点查询短暂变多;如果它保护的是短信发送、登录尝试或昂贵的报表生成,攻击者只要盯住窗口边界,就能稳定获得双倍额度。Redis 没有算错,错的是我们把离散窗口当成了连续时间。
// 一个窗口一个 key;边界一过,计数重新开始
const windowNo = Math.floor(nowMs / windowMs);
const key = `rate:product:42:${windowNo}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.pExpire(key, windowMs);
}
return count <= quota
? { allowed: true }
: { allowed: false, status: 429 };