功能说明
该示例实现按 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 };
}
}