3. 限流器原理:常见限流算法(令牌桶、漏桶、滑动窗口)原理对比与适用场景

限流(Rate Limiting)是保护系统不被突发流量冲垮的第一道防线。与熔断侧重“失败后的快速止损”不同,限流侧重“在流量到达服务前进行主动控制”。理解不同限流算法的数学原理与资源开销,是后续压测分析 QPS 拐点的基础。

3.1 三种核心限流算法概述

业界主流限流算法可归纳为三类:令牌桶( Bucket)、漏桶(Leaky Bucket)和滑动窗口(Sliding Window)。下表从核心机制、突发处理能力、内存开销和典型实现四个维度进行对比:

对比维度 令牌桶 漏桶 滑动窗口
核心机制 以固定速率生成令牌,请求需获取令牌才能通过 请求以固定速率流出,超出桶容量的请求被丢弃 将时间划分为小格子,统计滑动时间窗口内的请求总数
突发处理 允许一定程度的突发(桶内令牌累积) 严格平滑,不允许突发 取决于窗口粒度,粒度越细突发容忍度越低
内存开销 低(仅维护令牌数和上次填充时间戳) 低(仅维护队列长度和流出速率) 中到高(需维护窗口内所有时间片的计数器)
典型实现 Guava RateLimiter、Redis-Cell Nginx ngx_http_limit_req_module Sentinel 滑动窗口、Go 的 time/rate
压测视角提示:在性能压测中,令牌桶的“突发容忍”特性容易导致短时 QPS 超过设定阈值,而漏桶则会将流量强制削平。选择哪种算法,直接决定了压测曲线是“尖刺型”还是“平缓型”。

3.2 令牌桶算法( Bucket)

令牌桶是最灵活的限流算法,允许一定程度的突发流量,同时保证长期平均速率可控。

  • 工作原理:系统以恒定速率 r 向桶中放入令牌,桶容量上限为 b。每个请求到达时,从桶中取走一个令牌;若桶空,则请求被拒绝或等待。
  • 关键参数:令牌生成速率 r(决定平均 QPS)、桶容量 b(决定突发大小)。
  • 适用场景:API 网关、微服务间调用、需要容忍短时流量突刺的业务(如秒杀开始瞬间)。
// 伪代码:令牌桶核心逻辑
class TokenBucket {
    long capacity;      // 桶容量
    long tokens;        // 当前令牌数
    long lastRefill;    // 上次填充时间戳
    double refillRate;  // 令牌生成速率(个/秒)

    boolean tryAcquire() {
        refill(); // 先补充令牌
        if (tokens > 0) {
            tokens--;
            return true;
        }
        return false;
    }

    void refill() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastRefill;
        long newTokens = (long)(elapsed * refillRate / 1000);
        tokens = Math.min(capacity, tokens + newTokens);
        lastRefill = now;
    }
}
压测陷阱:令牌桶的“预消费”模式(如 Guava RateLimiter 的 warmup 阶段)会导致压测初期 QPS 偏低,后期突然升高。压测时必须预热足够时间,否则拐点数据会严重失真。

3.3 漏桶算法(Leaky Bucket)

漏桶算法将请求视为水滴,以恒定速率从桶底漏出,桶满则溢出丢弃。它强制输出流量完全平滑,没有任何突发。

  • 工作原理:请求先进入一个 FIFO 队列(桶),后台以固定速率 r 从队列头部取出请求进行处理。队列长度上限为 b,超出部分直接丢弃。
  • 关键参数:流出速率 r(决定最大处理 QPS)、队列深度 b(决定最大等待请求数)。
  • 适用场景:数据库写入限流、日志收集、消息队列消费端限流——这些场景要求输出速率稳定。
// 伪代码:漏桶核心逻辑
class LeakyBucket {
    long capacity;      // 桶容量(队列最大长度)
    long water;         // 当前桶中水量(队列长度)
    long lastLeak;      // 上次漏水时间戳
    double leakRate;    // 漏水速率(请求/秒)

    boolean tryAcquire() {
        leak(); // 先漏水
        if (water < capacity) {
            water++;
            return true;
        }
        return false;
    }

    void leak() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastLeak;
        long leaked = (long)(elapsed * leakRate / 1000);
        water = Math.max(0, water - leaked);
        lastLeak = now;
    }
}
性能开销对比:漏桶的队列实现通常需要加锁保护 water 计数器的原子性,在高并发下锁竞争比令牌桶的 CAS 操作更重。压测数据显示,漏桶在 10 万 QPS 级别时,CPU 开销比令牌桶高约 15%-20%。

3.4 滑动窗口算法(Sliding Window)

滑动窗口是对固定时间窗口(如 1 秒)的改进,通过细粒度时间片来避免“窗口边界流量突刺”问题。

  • 工作原理:将时间窗口划分为 n 个等长的小格子(如 1 秒窗口分为 10 个 100ms 格子)。每个格子维护一个计数器。滑动窗口向前移动时,丢弃最旧的格子,加入最新的格子,统计所有格子计数之和。
  • 关键参数:窗口总时长 W、格子数量 n(决定精度)、阈值 limit。
  • 适用场景:需要精确控制单位时间内的请求总数,且不能容忍突发(如登录接口防刷、短信验证码限流)。
// 伪代码:滑动窗口核心逻辑
class SlidingWindow {
    int windowSize;          // 窗口总时长(毫秒)
    int slotCount;           // 格子数量
    int[] slotCounters;      // 每个格子的计数
    long[] slotTimestamps;   // 每个格子的起始时间戳
    int currentSlot;         // 当前格子索引
    int totalCount;          // 窗口内总请求数

    boolean tryAcquire() {
        long now = System.currentTimeMillis();
        advanceWindow(now);  // 滑动窗口
        if (totalCount < limit) {
            slotCounters[currentSlot]++;
            totalCount++;
            return true;
        }
        return false;
    }

    void advanceWindow(long now) {
        // 计算当前时间对应的格子索引
        int newSlot = (int)((now / slotDuration) % slotCount);
        while (currentSlot != newSlot) {
            // 移动到下一个格子,丢弃最旧格子数据
            currentSlot = (currentSlot + 1) % slotCount;
            totalCount -= slotCounters[currentSlot];
            slotCounters[currentSlot] = 0;
            slotTimestamps[currentSlot] = now;
        }
    }
}

3.5 算法对比与选型决策树

以下 SVG 图展示了三种算法的核心流程对比,以及基于业务需求的选型决策路径:

三种限流算法流程对比与选型决策 限流算法流程对比与选型决策树 业务限流需求 需要突发容忍? 需要精确计数? 是 否 是 否 令牌桶 允许突发,平均速率可控 漏桶 强制平滑,无突发 滑动窗口 精确计数,粒度可控 否 → 漏桶 选型建议: • 网关/API 限流:优先令牌桶(突发容忍 + 低内存开销) • 数据库/消息队列:优先漏桶(输出平滑) • 防刷/精确计数:优先滑动窗口(粒度可控,无边界突刺) 性能开销对比(10万 QPS 级别) 令牌桶:CPU 低,内存 2 变量 漏桶:CPU 中,内存 2 变量 滑动窗口:CPU 高,内存 N 变量

3.6 压测视角的关键差异总结

从性能压测和资源开销角度,三种算法在以下三个维度存在显著差异:

  1. QPS 拐点形态:
    • 令牌桶:拐点呈“缓升-陡升”形态,突发令牌耗尽后 QPS 迅速回落至设定阈值。
    • 漏桶:拐点呈“平顶”形态,一旦队列满,QPS 立即被削平,无任何尖刺。
    • 滑动窗口:拐点呈“锯齿”形态,窗口边界处可能出现小幅波动,格子越细锯齿越小。
  2. CPU 开销:
    • 令牌桶:每次请求仅需一次 CAS 操作更新令牌数,CPU 开销最低。
    • 漏桶:需要维护队列状态,加锁开销略高于令牌桶。
    • 滑动窗口:每次请求需更新当前格子计数器并可能滑动窗口,格子数越多 CPU 开销越大。
  3. 内存开销:
    • 令牌桶和漏桶:仅需 2 个变量(当前计数 + 时间戳),内存占用可忽略。
    • 滑动窗口:需要维护 n 个格子的计数器和时间戳数组,n 通常为 10~100,内存开销随精度线性增长。
压测实验设计建议:在后续章节的压测实验中,建议对三种算法分别设置相同的 QPS 阈值(如 1000 QPS),然后使用阶梯递增负载(从 500 QPS 到 5000 QPS),观察并记录每种算法的实际通过 QPS 曲线、CPU 使用率和内存占用。这将直观展示“QPS 拐点”与“资源开销”之间的 trade-off。