← 中文资源

简体中文版本

滑动窗口限流计数

用时间戳滑动窗口按 key 实施请求限流,平滑固定窗口在边界处允许的突发。

阅读英文原文

功能说明

该示例实现按 key 的滑动窗口限流。与固定窗口不同,它保存每次命中的时间戳,判定时只统计落在窗口内的命中,因此窗口边界处不会出现两倍突发的漏洞。被拒绝时返回需等待的毫秒数,便于调用方设置重试间隔或回以 429 与 Retry-After。

关键代码

每次判定先用当前时间减去窗口得到截断点,过滤掉过期时间戳,从而自然滑动窗口。命中数达到上限即拒绝,并依据最早一次命中的过期时间计算等待时长。允许时记录当前时间戳。数据结构按 key 分桶,容量与 key 数量成正比,适合单进程内的粗粒度限流。构造参数做了正整数校验,避免非法窗口导致逻辑失真。

使用注意

该实现在单进程内存态工作,多实例部署会各自计数,需改用共享存储或集中式限流服务才能全局生效。大量分散 key 会让 Map 无界增长,应配合过期清理。精确到单个请求的严格限流在热点 key 上会有内存压力,可按业务选择令牌桶或近似滑动窗口。限流是保护而非拒绝,拒绝时应返回明确的重试信息,而非静默丢包。

代码示例

export class SlidingWindowLimiter {
  constructor(limit, windowMs) {
    if (!Number.isInteger(limit) || limit <= 0) throw new RangeError('limit must be positive');
    if (!Number.isInteger(windowMs) || windowMs <= 0) throw new RangeError('windowMs must be positive');
    this.limit = limit;
    this.windowMs = windowMs;
    this.hits = new Map();
  }

  allow(key, now = Date.now()) {
    const cutoff = now - this.windowMs;
    const times = (this.hits.get(key) ?? []).filter((t) => t > cutoff);
    if (times.length >= this.limit) {
      this.hits.set(key, times);
      return { allowed: false, retryAfterMs: times[0] + this.windowMs - now };
    }
    times.push(now);
    this.hits.set(key, times);
    return { allowed: true, remaining: this.limit - times.length };
  }
}