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 系统架构中,选择限流算法需考虑以下因素:

  1. 下游处理能力是否固定?是 → 漏桶算法;否 → 进入下一步。
  2. 是否允许短时突发?是 → 令牌桶算法;否 → 进入下一步。
  3. 是否需要精确平滑?是 → 滑动窗口算法;否 → 计数器算法(仅限低要求场景)。
  4. 分布式环境?是 → 优先选择令牌桶或滑动窗口(基于 Redis 实现)。
生产建议:在 OAuth2 授权服务器中,推荐将令牌桶作为主限流算法(token 端点、introspection 端点),滑动窗口作为辅助(/authorize 端点),漏桶用于签名/加密等固定资源操作。计数器算法仅用于监控告警阈值,不作为实际限流手段。

4.7 四种算法核心流程对比(SVG)

下图从请求到达、判断逻辑、通过/拒绝三个维度对比四种算法的核心流程:

四种限流算法核心流程对比 四种限流算法核心流程对比 计数器算法 固定窗口 + 计数器 请求到达 → 计数器+1 超过阈值? 通过 拒绝 窗口结束 → 计数器归零 滑动窗口算法 N 个子窗口 + 滑动统计 请求到达 → 定位当前子窗口 N 个子窗口计数之和 通过 拒绝 丢弃最旧子窗口,加入最新 漏桶算法 桶容量 + 恒定漏水速率 请求到达 → 桶是否满? 未满 → 入桶等待漏出 通过 拒绝 恒定速率漏出请求 令牌桶算法 桶容量 + 恒定令牌生成速率 请求到达 → 桶中有令牌? 有 → 消耗令牌 通过 拒绝 令牌持续生成,桶满则溢出 四种算法均包含“请求到达 → 判断 → 通过/拒绝”核心三阶段,区别在于判断逻辑与状态维护方式 计数器:窗口边界突变 滑动窗口:子窗口平滑 漏桶:强制恒定速率 令牌桶:可调突发能力

4.8 令牌桶与漏桶的流量整形对比(SVG)

下图直观展示令牌桶与漏桶在应对突发流量时的行为差异:

令牌桶 vs 漏桶:突发流量处理对比 令牌桶 vs 漏桶:突发流量处理对比 令牌桶算法 令牌桶 容量 b=5 当前令牌: 5 生成速率 r=1/s 突发 5 个请求 全部通过 后续请求 等待令牌 令牌桶允许突发 b 个请求通过,后续请求需等待令牌生成 输出速率:突发 → 平滑过渡到 r 漏桶算法 漏桶 容量 b=5 当前水量: 5 漏水速率 r=1/s 突发 5 个请求 入桶排队 恒定 1/s 漏出 桶满丢弃 漏桶将突发请求放入桶中排队,以恒定速率漏出 输出速率:始终恒定 r,桶满则丢弃

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 桶