2. 常见限流算法:原理与对比

在金融场景中,API 网关或微服务层通常需要部署限流算法来精确控制请求速率。不同算法在突发处理能力、内存开销、边界精度上差异显著。本节从原理出发,对比四种主流算法的适用场景与工程代价。

2.1 计数器算法(Counter Algorithm)

计数器算法是最简单的实现方式:将时间划分为固定窗口(如 1 秒),每个窗口维护一个计数器,请求到达时计数器加 1;若计数器超过阈值则拒绝请求;窗口结束时重置计数器。

核心特征:实现极简,仅需一个计数器 + 定时器;但存在明显的“窗口边界突发”问题——在窗口切换瞬间可能涌入 2 倍阈值的请求。

计数器算法流程

  1. 获取当前时间窗口索引(如 timestamp / windowSize)。
  2. 若窗口索引与上次不同,重置计数器为 0。
  3. 计数器加 1。
  4. 若计数器 > 阈值,拒绝请求;否则放行。

伪代码示例

// 计数器限流(1秒窗口,阈值100)
long currentWindow = System.currentTimeMillis() / 1000;
if (currentWindow != lastWindow) {
    counter = 0;
    lastWindow = currentWindow;
}
if (++counter > 100) {
    return false; // 限流
}
return true; // 放行

2.2 滑动窗口算法(Sliding Window Algorithm)

滑动窗口算法将固定窗口细分为多个小格子(如 1 秒窗口分为 10 个 100ms 格子),每个格子独立计数。窗口随请求时间滑动,统计当前时间点前一个完整窗口内的总请求数。这消除了计数器算法的边界突发问题,但需要更多内存存储子窗口计数。

工程建议:滑动窗口的格子数通常设为 5~20。格子越多精度越高,但内存开销线性增长。金融核心交易建议使用 10 格以上。

滑动窗口数据结构

// 滑动窗口(1秒窗口,10格)
class SlidingWindow {
    long[] counters = new long[10]; // 每格100ms
    long windowSize = 1000;         // 1秒
    int slotCount = 10;
    long slotSize = 100;            // 100ms

    boolean allow() {
        long now = System.currentTimeMillis();
        int slotIndex = (int)((now % windowSize) / slotSize);
        // 清理过期格子
        // 统计当前窗口总请求数
        // 若 < 阈值则当前格子+1并放行
    }
}

2.3 漏桶算法(Leaky Bucket Algorithm)

漏桶算法将请求视为水滴,以恒定速率从桶底漏出;桶容量有限,当桶满时新请求被丢弃。漏桶强制输出为平滑的恒定速率,适合保护下游系统不被突发流量冲垮,但无法应对突发短时高并发。

金融场景注意:漏桶算法会“削峰填谷”,对需要快速响应突发交易的场景(如秒杀、抢购)不友好。建议用于对账、批量处理等非实时链路。

漏桶算法状态

  • 桶容量(capacity):最大积压请求数。
  • 漏水速率(leakRate):每秒处理请求数。
  • 当前水量(water):桶中积压的请求数。
  • 上次漏水时间(lastLeakTime):用于计算应漏水量。

伪代码示例

// 漏桶限流(容量100,速率50/s)
long now = System.currentTimeMillis();
// 先漏水:根据时间差计算应漏水量
long elapsed = now - lastLeakTime;
water = Math.max(0, water - elapsed * leakRate / 1000);
lastLeakTime = now;

if (water < capacity) {
    water++;
    return true; // 放行
}
return false; // 桶满,拒绝

2.4 令牌桶算法(Token Bucket Algorithm)

令牌桶算法以固定速率向桶中添加令牌,请求到达时需消耗一个令牌才能放行。桶有最大令牌数限制。与漏桶不同,令牌桶允许一定程度的突发——只要桶中有累积令牌,可一次性消耗多个令牌应对突发流量。

金融推荐:令牌桶是业界最通用的限流算法,兼顾平滑与突发。Guava RateLimiter、Sentinel 均基于令牌桶或变种实现。适合交易、查询、风控等混合场景。

令牌桶算法状态

  • 桶容量(capacity):最大令牌存储数,决定突发上限。
  • 令牌生成速率(rate):每秒放入令牌数。
  • 当前令牌数(tokens):桶中可用令牌。
  • 上次填充时间(lastRefillTime):用于计算应补充令牌数。

伪代码示例

// 令牌桶限流(容量200,速率100/s)
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
tokens = Math.min(capacity, tokens + elapsed * rate / 1000);
lastRefillTime = now;

if (tokens >= 1) {
    tokens--;
    return true; // 放行
}
return false; // 无令牌,拒绝

2.5 四种算法对比总表

对比维度 计数器 滑动窗口 漏桶 令牌桶
实现复杂度 极低 中 低 中
内存开销 O(1) O(n)(格子数) O(1) O(1)
边界突发问题 严重 无(精度取决于格子数) 无(强制平滑) 允许可控突发
输出速率 不均匀 较均匀 严格恒定 平均恒定,可突发
适用金融场景 非关键路径、日志上报 核心交易、风控查询 对账、批量、数据同步 交易、秒杀、实时风控
典型实现 本地 AtomicLong Redis Sorted Set / 本地环形数组 队列 + 定时消费 Guava RateLimiter / Sentinel

2.6 算法选择决策树

在金融系统架构中,可按以下逻辑快速选择限流算法:

  • 需要严格恒定输出速率? → 漏桶算法(对账、批量导出)。
  • 需要容忍突发且实现简单? → 令牌桶算法(交易、查询、秒杀)。
  • 需要高精度且无边界突发? → 滑动窗口算法(风控、敏感操作)。
  • 仅做粗略保护,资源受限? → 计数器算法(非核心、边缘服务)。
架构建议:生产环境推荐以令牌桶为主、滑动窗口为辅的组合策略。核心交易链路使用令牌桶应对突发;风控/防刷场景使用滑动窗口精确控制窗口内频次。

2.7 四种算法核心流程对比图

四种限流算法核心流程对比 四种限流算法核心流程对比 计数器算法 固定窗口 + 计数器 窗口切换时重置 计数器 > 阈值? 拒绝 放行 ⚠ 窗口边界突发:2倍阈值 内存 O(1),实现极简 滑动窗口算法 N 个子窗口(格子) 滑动统计总请求数 总请求 > 阈值? 拒绝 放行 ✅ 无边界突发,精度可控 内存 O(n),格子数决定精度 漏桶算法 请求入桶(积压) 恒定速率漏水 桶满? 丢弃 排队处理 ⏳ 输出严格恒定 无法应对突发,适合批量 令牌桶算法 定时生成令牌 请求消耗令牌 有令牌? 拒绝 放行 🚀 允许突发(累积令牌) 业界最通用,推荐金融场景 金融场景选型总结 核心交易 → 令牌桶(突发友好) | 风控防刷 → 滑动窗口(精确控制) 批量对账 → 漏桶(恒定输出) | 边缘服务 → 计数器(资源受限) 推荐组合:令牌桶 + 滑动窗口分层限流

2.8 金融场景选型建议

金融业务场景 推荐算法 关键考量
交易下单、支付 令牌桶 允许秒杀突发,平均速率可控
风控规则查询 滑动窗口 精确控制窗口内频次,防刷
对账文件导出 漏桶 恒定输出,保护下游文件系统
日志上报、监控 计数器 资源开销最低,允许少量丢失
用户登录、验证码 滑动窗口 + 令牌桶 窗口内频次控制 + 总体速率限制
生产警告:切勿在金融核心链路直接使用计数器算法。2021 年某券商因计数器边界突发导致风控阈值瞬间击穿,造成 3000 万异常交易。滑动窗口或令牌桶是金融生产的最低要求。