2. 常见限流算法详解:计数器算法、滑动窗口算法、漏桶算法、令牌桶算法的原理与数学推导

在金融API网关与微服务架构中,限流算法的选择直接决定了系统的抗冲击能力与资源利用率。本章从数学原理出发,逐一剖析四种经典限流算法的数学模型、实现要点与适用场景。

2.1 计数器算法(Counter Algorithm)

计数器算法是最直观的限流实现,在固定时间窗口内维护一个计数器,当请求到达时计数器加1,若计数器超过阈值则拒绝请求,窗口结束后计数器重置。

数学推导

设时间窗口长度为 T(单位:秒),窗口内最大允许请求数为 N。在任意时刻 t,当前窗口内的请求计数为 C(t)。限流判定条件为:

C(t) < N  → 允许请求,C(t) = C(t) + 1
C(t) ≥ N  → 拒绝请求

窗口重置逻辑:当 t mod T = 0 时,C(t) = 0。

临界突变问题

计数器算法存在明显的“窗口边界突变”缺陷:在窗口切换的瞬间,流量可能瞬间翻倍。例如 T=1s, N=100,若第0.9s~1.0s内通过100个请求,第1.0s~1.1s内又通过100个请求,则实际0.2s内通过了200个请求,远超平均速率。

金融场景警示: 计数器算法的窗口突变特性在秒杀、抢购等瞬时高并发场景下,可能导致后端数据库连接池瞬间打满,引发雪崩效应。不建议在核心交易链路单独使用。

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

滑动窗口算法将固定窗口细分为多个子窗口,通过滑动子窗口的统计来平滑流量,消除计数器算法的边界突变问题。

数学推导

将时间窗口 T 划分为 m 个等长子窗口,每个子窗口长度为 T/m。维护一个长度为 m 的环形数组 counts[],记录每个子窗口内的请求数。当前时间 t 对应的子窗口索引为:

index = floor(t / (T/m)) % m

当前窗口总请求数 S(t) 为所有子窗口计数之和:

S(t) = Σ counts[i]  (i = 0, 1, ..., m-1)

限流判定:S(t) < N 则允许,否则拒绝。

精度与开销权衡

子窗口数 m 精度提升 内存开销 推荐场景
2~5 低 极低 非核心监控、日志限流
10~20 中 低 普通API限流
50~100 高 中 金融核心交易、高频交易
工程建议: 金融API推荐使用 m=20 的子窗口划分,在精度与内存开销之间取得平衡。单机内存占用仅需 20 × 8字节 ≈ 160字节,完全可接受。
滑动窗口算法原理(m=5) 子窗口 0 子窗口 1 子窗口 2 子窗口 3 子窗口 4(当前) 滑动窗口总范围(T=1s) 计数: 23 计数: 18 计数: 31 计数: 27 计数: 12(进行中) S(t) = 23 + 18 + 31 + 27 + 12 = 111 阈值 N=100 → 111 ≥ 100 → 拒绝请求

2.3 漏桶算法(Leaky Bucket Algorithm)

漏桶算法将请求视为水滴,以恒定速率从桶底漏出,桶容量有限,超出容量的请求被丢弃。该算法强制输出流量完全平滑,适用于需要严格整形流量的场景。

数学推导

定义漏桶参数:

  • 容量 C:桶的最大存储请求数
  • 漏出速率 r:单位时间漏出的请求数(请求/秒)
  • 当前水量 W(t):t 时刻桶内的请求数

漏桶的微分方程:

dW(t)/dt = λ(t) - r

其中 λ(t) 为 t 时刻的请求到达速率。离散化实现:

// 每次请求到达时执行
current_time = now()
elapsed = current_time - last_leak_time
water = max(0, water - elapsed * r)  // 先漏水
if water < C:
    water += 1  // 允许请求
    last_leak_time = current_time
    return true
else:
    return false  // 桶满,拒绝

关键特性

特性 描述 金融场景影响
输出速率恒定 无论输入流量多大,输出始终 ≤ r 保护下游系统不被突发流量冲垮
允许突发输入 桶容量 C 可吸收瞬时突发 适合处理短时脉冲,但延迟会增加
无法应对空载 桶空时无法加速输出 空闲时段资源无法被利用
数学本质: 漏桶算法本质是一个 积分器 + 限幅器。积分器累积请求,限幅器确保输出速率不超过 r。从控制理论角度看,它是一个一阶低通滤波器,截止频率为 f_c = r / (2πC)。

2.4 令牌桶算法(Token Bucket Algorithm)

令牌桶算法以恒定速率向桶中添加令牌,请求到达时需消耗一个令牌才能通过。与漏桶不同,令牌桶允许一定程度的突发流量,是业界最广泛使用的限流算法。

数学推导

定义令牌桶参数:

  • 桶容量 B:桶中最多存放的令牌数
  • 令牌生成速率 r:每秒生成的令牌数
  • 当前令牌数 T(t):t 时刻桶内的令牌数

令牌生成过程:

T(t) = min(B, T(t₀) + r × (t - t₀))

请求消耗令牌:

if T(t) ≥ 1:
    T(t) = T(t) - 1
    return true  // 允许请求
else:
    return false  // 令牌不足,拒绝

突发能力分析

令牌桶允许的最大突发请求数等于桶容量 B。当桶满时,可一次性处理 B 个请求,之后输出速率被限制为 r。长期平均速率同样为 r。

令牌桶算法 vs 漏桶算法对比 令牌桶算法 令牌 令牌 令牌 空位 生成器 r 请求 允许(消耗1令牌) 漏桶算法 请求 请求 请求 空位 漏口 r 请求 恒定速率输出

2.5 四种算法对比与选型

算法 输出平滑度 突发能力 实现复杂度 内存开销 金融推荐度
计数器 差(边界突变) 高(窗口边界) 极低 极低 ★☆☆☆☆
滑动窗口 中(取决于m) 中 低 低 ★★★☆☆
漏桶 极高(严格整形) 低(受限于容量) 中 低 ★★★★☆
令牌桶 高 高(可配置B) 中 低 ★★★★★
金融选型建议: 令牌桶算法是金融API限流的首选方案。它既能通过 r 控制长期平均速率,又能通过 B 允许合理的突发流量(如批量查询、缓存预热)。建议初始配置 r = 业务峰值TPS × 1.2,B = r × 0.5,再通过压测微调。

2.6 分布式环境下的算法演进

在分布式金融系统中,单机限流需要扩展为集群限流。核心挑战在于:如何在高并发下维持全局计数的一致性?

分布式令牌桶实现方案

  1. Redis + Lua 脚本:利用 Redis 单线程模型和 Lua 原子性,实现分布式令牌桶。每次请求执行一次 Lua 脚本,保证令牌获取的原子性。
  2. 本地令牌桶 + 定期同步:每个节点维护本地令牌桶,通过配置中心(如 etcd/Consul)定期同步全局配额。牺牲一定精度换取性能。
  3. Sentinel 集群流控:阿里开源的 Sentinel 提供 Token Server/Client 模式,由 Token Server 统一分配令牌,Client 本地缓存。
// Redis + Lua 分布式令牌桶核心逻辑
local key = KEYS[1]                -- 令牌桶key
local rate = tonumber(ARGV[1])     -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])      -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4])-- 请求消耗令牌数

local last_time = redis.call('hget', key, 'last_time') or now
local tokens = redis.call('hget', key, 'tokens') or capacity

-- 计算时间差,生成新令牌
local elapsed = math.max(0, now - last_time)
tokens = math.min(capacity, tokens + elapsed * rate / 1000)

-- 判断是否允许
if tokens >= requested then
    tokens = tokens - requested
    redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
    return 1  -- 允许
else
    redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
    return 0  -- 拒绝
end
分布式限流陷阱: Redis 单点故障会导致全局限流失效。生产环境必须部署 Redis 集群(至少 3 主 3 从),并使用 RedLock 或哨兵模式保证高可用。同时,Lua 脚本执行时间应控制在 10ms 以内,避免阻塞 Redis 主线程。

2.7 本章小结