4. 限流基础理论:令牌桶算法、漏桶算法、滑动窗口算法、计数器算法原理与适用场景
在 OAuth2 授权与资源访问链路中,限流是防止系统过载、保障尾延迟稳定的第一道防线。不同的限流算法在突发处理能力、内存开销、边界平滑度上存在本质差异。本章深入剖析四种核心算法的数学原理、实现要点与适用场景,为后续分布式限流与压测诊断奠定理论基础。
4.1 计数器算法(Counter Algorithm)
计数器算法是最直观的限流实现,在固定时间窗口内累计请求次数,超过阈值则拒绝后续请求。
原理
- 将时间划分为固定长度的窗口(如 1 秒、1 分钟)。
- 每个窗口维护一个计数器,请求到达时计数器加 1。
- 若计数器超过预设阈值,则拒绝该窗口内的后续请求。
- 窗口结束时计数器归零,进入下一个窗口。
临界突变问题
计数器算法存在明显的“窗口边界突变”缺陷:若在窗口末尾突发大量请求,紧接着下一个窗口开始又涌入大量请求,实际瞬时 QPS 可能达到阈值的 2 倍,导致系统过载。
典型场景:OAuth2 客户端凭证模式下的 token 端点限流。若窗口设为 1 秒、阈值 100,在 0.9s~1.1s 的 200ms 内可能涌入 200 个请求,后端 token 签发服务瞬间压力翻倍。
适用场景
- 对突发容忍度较高的非关键路径(如日志上报、非实时统计)。
- 单机简单限流,实现成本最低。
- 配合其他算法作为兜底策略。
4.2 滑动窗口算法(Sliding Window Algorithm)
滑动窗口算法通过将时间窗口细分为多个小格,并随请求时间动态滑动,解决了计数器算法的边界突变问题。
原理
- 将固定窗口(如 1 秒)划分为 N 个更小的子窗口(如 10 个 100ms 子窗口)。
- 每个子窗口独立记录请求计数。
- 当前时间点对应的完整窗口为:当前子窗口 + 前 N-1 个子窗口的计数之和。
- 随着时间推移,最旧的子窗口被丢弃,最新的子窗口加入统计。
精度与内存权衡
子窗口数量 N 越大,限流精度越高,但内存占用也线性增长。实际生产环境通常取 N=10~100 之间。
OAuth2 实践:授权码模式下的 /authorize 端点限流。滑动窗口能有效防止攻击者在窗口边界发起“脉冲式”授权请求,保护用户确认页面的渲染资源。
适用场景
- 需要平滑限流、对突发敏感的 API 端点(如 OAuth2 授权端点、token 刷新端点)。
- 分布式限流中配合 Redis Sorted Set 实现。
- 对内存开销有一定容忍度的服务。
4.3 漏桶算法(Leaky Bucket Algorithm)
漏桶算法将请求视为水滴,以恒定速率从桶底漏出,桶满则溢出丢弃。它强制输出流量完全平滑。
原理
- 桶容量(burst capacity):允许瞬时积压的最大请求数。
- 漏水速率(leak rate):单位时间从桶底流出的请求数,即恒定处理速率。
- 请求到达时,若桶未满则放入桶中;若桶已满则直接丢弃(或排队等待)。
- 无论上游请求如何突发,下游始终以固定速率处理。
与令牌桶的核心区别
| 维度 | 漏桶算法 | 令牌桶算法 |
|---|---|---|
| 输出特性 | 强制平滑,无突发 | 允许一定突发 |
| 实现机制 | 请求入桶,恒定速率漏出 | 令牌生成,请求消耗令牌 |
| 对突发请求 | 桶满则丢弃 | 桶内令牌足够时可突发 |
| 适用场景 | 下游处理能力严格受限 | 允许短时突发,追求吞吐 |
适用场景
- 数据库连接池、消息队列消费端等下游处理能力固定的场景。
- OAuth2 中 token 签发的签名计算资源(如 RSA 或 EC 签名)——签名速度固定,漏桶可防止请求积压导致签名队列膨胀。
- 需要严格保护第三方 API 配额(如每秒钟最多调用 10 次)。
4.4 令牌桶算法(Token Bucket Algorithm)
令牌桶算法是业界最广泛使用的限流算法,兼顾平均速率限制与短时突发能力。Nginx、Guava RateLimiter、Redis 分布式限流均以此为核心。
原理
- 桶内以固定速率(r tokens/sec)生成令牌,桶容量上限为 b。
- 每个请求到达时,从桶中消耗 1 个(或多个)令牌。
- 若桶中有足够令牌,请求被允许通过;否则请求被拒绝或等待。
- 桶满时新生成的令牌溢出丢弃,不累积。
突发能力分析
令牌桶允许的最大突发请求数 = 桶容量 b。当桶满时,可一次性处理 b 个请求,之后速率被限制为 r。这种特性非常适合 OAuth2 场景中偶尔的流量尖峰(如大批客户端同时刷新 token)。
注意:令牌桶的突发能力可能导致下游瞬间压力。在 OAuth2 资源服务器端,若令牌桶容量设置过大,可能引发资源 API 的瞬时过载。建议将桶容量设置为平均 QPS 的 2~5 倍,并配合下游熔断机制。
适用场景
- OAuth2 授权服务器 token 端点、introspection 端点、revocation 端点。
- API Gateway 对上游客户端的限流。
- 需要平衡平均速率与突发响应的通用限流场景。
4.5 四种算法对比总览
| 算法 | 平滑度 | 突发支持 | 内存开销 | 实现复杂度 | 典型 OAuth2 场景 |
|---|---|---|---|---|---|
| 计数器 | 差(边界突变) | 无 | 极低 | 最低 | 非关键路径兜底 |
| 滑动窗口 | 较好(子窗口粒度) | 有限 | 中等 | 中等 | 授权端点、刷新端点 |
| 漏桶 | 最好(强制平滑) | 无 | 低 | 低 | 签名计算、数据库连接 |
| 令牌桶 | 良好(可调突发) | 有(桶容量控制) | 低 | 中等 | token 端点、API Gateway |
4.6 算法选择决策流程
在实际 OAuth2 系统架构中,选择限流算法需考虑以下因素:
- 下游处理能力是否固定?是 → 漏桶算法;否 → 进入下一步。
- 是否允许短时突发?是 → 令牌桶算法;否 → 进入下一步。
- 是否需要精确平滑?是 → 滑动窗口算法;否 → 计数器算法(仅限低要求场景)。
- 分布式环境?是 → 优先选择令牌桶或滑动窗口(基于 Redis 实现)。
生产建议:在 OAuth2 授权服务器中,推荐将令牌桶作为主限流算法(token 端点、introspection 端点),滑动窗口作为辅助(/authorize 端点),漏桶用于签名/加密等固定资源操作。计数器算法仅用于监控告警阈值,不作为实际限流手段。
4.7 四种算法核心流程对比(SVG)
下图从请求到达、判断逻辑、通过/拒绝三个维度对比四种算法的核心流程:
4.8 令牌桶与漏桶的流量整形对比(SVG)
下图直观展示令牌桶与漏桶在应对突发流量时的行为差异:
4.9 代码示例:令牌桶算法核心实现
以下为单机令牌桶的简化实现,展示核心逻辑:
public class TokenBucket {
private final long capacity; // 桶容量
private final double refillRate; // 令牌生成速率(个/秒)
private double tokens; // 当前令牌数
private long lastRefillTimestamp; // 上次补充令牌的时间戳
public TokenBucket(long capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity; // 初始满桶
this.lastRefillTimestamp = System.nanoTime();
}
public synchronized boolean tryConsume(int tokens) {
refill(); // 先补充令牌
if (this.tokens < tokens) {
return false; // 令牌不足,拒绝
}
this.tokens -= tokens;
return true;
}
private void refill() {
long now = System.nanoTime();
double elapsedSeconds = (now - lastRefillTimestamp) / 1_000_000_000.0;
double tokensToAdd = elapsedSeconds * refillRate;
this.tokens = Math.min(capacity, this.tokens + tokensToAdd);
this.lastRefillTimestamp = now;
}
}
生产级注意:上述实现适用于单机场景。在分布式 OAuth2 环境中,需使用 Redis Lua 脚本或 Redisson 的 RRateLimiter 实现原子化的令牌桶操作,避免并发竞争导致限流失效。
4.10 限流算法在 OAuth2 链路中的部署位置
| OAuth2 组件 | 推荐算法 | 限流粒度 | 关键指标 |
|---|---|---|---|
| 授权端点 /authorize | 滑动窗口 | 按 client_id + IP | 窗口大小 1s,子窗口 100ms |
| 令牌端点 /token | 令牌桶 | 按 client_id | 桶容量 = 平均 QPS × 3 |
| 令牌内省 /introspect | 令牌桶 | 按 resource_server_id | 桶 |