接口限流有哪些常见算法?令牌桶、漏桶、滑动窗口怎么选?
结论先行:限流的目标是让系统在超预期流量下“可控地变慢或被拒”,而不是被击穿。经典算法:固定窗口(计数器实现,简单但窗口边界有突刺)、滑动窗口(把窗口细分时间片,平滑且更精确)、漏桶(恒定速率流出,削峰强但不允许突发)、令牌桶(按速率补充令牌,允许一定突发)。接口层一般选令牌桶(允许突发、实现成熟);下游要求恒定速率时用漏桶;多实例分布式限流用 Redis + Lua 做原子计数或令牌。
算法对比表
| 算法 | 能否突发 | 实现复杂度 | 代表实现 |
|---|---|---|---|
| 固定窗口 | 边界可能双倍放行 | 低 | 计数器 |
| 滑动窗口 | 平滑、接近均速 | 中 | 时间片统计 |
| 漏桶 | 不允许突发 | 低 | 恒定出队速率 |
| 令牌桶 | 允许突发 | 低 | Guava RateLimiter |
令牌桶示例
// Guava 令牌桶:平均 QPS 上限 100,允许短暂突发
RateLimiter limiter = RateLimiter.create(100.0);
public Response handle() {
if (!limiter.tryAcquire(1, 200, TimeUnit.MILLISECONDS)) {
throw new TooManyRequestsException(); // 拿不到令牌快速失败,可映射 429
}
return doBusiness();
}
固定窗口与滑动窗口的实现差异
- 固定窗口:每秒一个计数器;第 59 秒与下一秒初的请求分属两个窗口,边界可能瞬间双倍放行;
- 滑动窗口:把时间切成小格(如 1 秒分 10 格),统计“当前时刻往前一个窗口”的格子总和,突刺被抹平;
- 代价:格子越多存储与计算开销越大,分布式场景还需 Lua 保证读写原子性。
分布式限流要点
- Redis 方案要“单条 Lua 脚本完成读、判、扣”,避免竞态导致超限;
- 令牌补充按“当前时间 - 上次补充时间”计算,再原子扣减;
- 被限流的请求返回明确错误码(如 429),方便调用方识别与调整策略;
- 阈值与触发量要有监控告警,限流规则随容量评估动态调整。
常见追问 / 记忆点
- 追问:单机限流在多实例下还准吗?答:不准,每个实例各自计数;全局限流需要 Redis + Lua 原子地扣减或取令牌。
- 追问:固定窗口为什么有边界突刺?答:窗口边界两侧各能放满一整窗口的流量,瞬间可能两倍流量穿过去。
- 记忆点:令牌桶“攒令牌、容突发”,漏桶“恒速、强削峰”;能接受小突发选令牌桶,下游要恒速选漏桶。