2. 常见限流算法:原理与对比
在金融场景中,API 网关或微服务层通常需要部署限流算法来精确控制请求速率。不同算法在突发处理能力、内存开销、边界精度上差异显著。本节从原理出发,对比四种主流算法的适用场景与工程代价。
2.1 计数器算法(Counter Algorithm)
计数器算法是最简单的实现方式:将时间划分为固定窗口(如 1 秒),每个窗口维护一个计数器,请求到达时计数器加 1;若计数器超过阈值则拒绝请求;窗口结束时重置计数器。
核心特征:实现极简,仅需一个计数器 + 定时器;但存在明显的“窗口边界突发”问题——在窗口切换瞬间可能涌入 2 倍阈值的请求。
计数器算法流程
- 获取当前时间窗口索引(如 timestamp / windowSize)。
- 若窗口索引与上次不同,重置计数器为 0。
- 计数器加 1。
- 若计数器 > 阈值,拒绝请求;否则放行。
伪代码示例
// 计数器限流(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.8 金融场景选型建议
| 金融业务场景 | 推荐算法 | 关键考量 |
|---|---|---|
| 交易下单、支付 | 令牌桶 | 允许秒杀突发,平均速率可控 |
| 风控规则查询 | 滑动窗口 | 精确控制窗口内频次,防刷 |
| 对账文件导出 | 漏桶 | 恒定输出,保护下游文件系统 |
| 日志上报、监控 | 计数器 | 资源开销最低,允许少量丢失 |
| 用户登录、验证码 | 滑动窗口 + 令牌桶 | 窗口内频次控制 + 总体速率限制 |
生产警告:切勿在金融核心链路直接使用计数器算法。2021 年某券商因计数器边界突发导致风控阈值瞬间击穿,造成 3000 万异常交易。滑动窗口或令牌桶是金融生产的最低要求。