常见限流算法详解与应用场景

常见限流算法详解与应用场景

在高并发分布式系统中,限流(Rate Limiting)是保障系统稳定性的核心手段之一。当流量突增超过系统承载能力时,合理的限流策略能够防止系统雪崩,确保核心服务的可用性。本文将深入剖析四种主流限流算法的原理、实现及适用场景,帮助你在实际项目中做出正确选择。

一、为什么需要限流?

1.1 限流的核心价值

限流的本质是流量整形(Traffic Shaping),通过控制单位时间内的请求量,实现以下目标:

  • 保护系统资源:防止数据库连接池耗尽、线程池打满、内存溢出
  • 保障服务可用性:避免单点故障引发级联崩溃(雪崩效应)
  • 实现公平调度:防止少数用户占用过多资源,影响其他用户体验
  • 应对突发流量:平滑处理秒杀、抢购等场景下的流量峰值

1.2 限流的应用场景

场景 限流策略 说明
API 网关 全局限流 保护后端所有微服务
用户级别 单用户限流 防止恶意刷接口、爬虫
接口级别 接口限流 保护热点接口(如登录、支付)
服务间调用 服务限流 防止下游服务被压垮
分布式锁 并发限流 控制同一资源的并发访问量

二、四种主流限流算法详解

2.1 计数器算法(Fixed Window Counter)

计数器算法是最简单直观的限流方式,通过统计固定时间窗口内的请求数进行限流。

2.1.1 原理

  • 将时间划分为固定窗口(如 1 秒)
  • 每个窗口维护一个计数器
  • 请求到达时,计数器 +1
  • 当计数器超过阈值时,拒绝后续请求
  • 窗口结束时,计数器重置为 0

2.1.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 计数器限流算法实现
*/
public class CounterRateLimiter {
// 时间窗口大小(毫秒)
private final long windowSize;
// 窗口内最大请求数
private final int maxRequests;
// 当前窗口开始时间
private volatile long windowStart;
// 当前窗口请求计数
private final AtomicInteger counter;

public CounterRateLimiter(long windowSizeMs, int maxRequests) {
this.windowSize = windowSizeMs;
this.maxRequests = maxRequests;
this.windowStart = System.currentTimeMillis();
this.counter = new AtomicInteger(0);
}

/**
* 尝试获取许可
* @return true: 允许通过, false: 被限流
*/
public boolean tryAcquire() {
long now = System.currentTimeMillis();

// 检查是否进入新窗口
if (now - windowStart >= windowSize) {
synchronized (this) {
// 双重检查
if (now - windowStart >= windowSize) {
windowStart = now;
counter.set(0);
}
}
}

// 尝试增加计数
int current = counter.incrementAndGet();
if (current <= maxRequests) {
return true;
}

// 超过阈值,回滚计数并拒绝
counter.decrementAndGet();
return false;
}
}

2.1.3 优缺点分析

优点 缺点
实现简单,性能高 存在临界突变问题(窗口边界突发流量)
内存占用低 无法平滑处理流量
易于理解和维护 精度受窗口大小影响

2.1.4 临界突变问题

假设窗口大小为 1 秒,限流阈值为 100:

1
2
3
4
时间线: |-------窗口1-------|-------窗口2-------|
请求数: 100 100

在窗口1的末尾和窗口2的开头,可能出现瞬间 200 请求,远超阈值!

2.2 滑动窗口算法(Sliding Window)

滑动窗口算法是对计数器算法的改进,通过将大窗口细分为多个小窗口,实现更平滑的限流效果。

2.2.1 原理

  • 将时间窗口划分为 N 个小窗口(如 1 秒分为 10 个 100ms 小窗口)
  • 每个小窗口维护独立的计数器
  • 请求到达时,计算当前所在小窗口并计数
  • 统计最近 N 个小窗口的总请求数,判断是否超过阈值
  • 窗口滑动时,淘汰最旧的小窗口数据

2.2.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 滑动窗口限流算法实现
*/
public class SlidingWindowRateLimiter {
// 大窗口大小(毫秒)
private final long windowSize;
// 小窗口数量
private final int subWindowCount;
// 小窗口大小(毫秒)
private final long subWindowSize;
// 限流阈值
private final int maxRequests;
// 小窗口计数器数组
private final AtomicInteger[] subWindows;
// 每个小窗口的开始时间
private final long[] windowStarts;

public SlidingWindowRateLimiter(long windowSizeMs, int subWindowCount, int maxRequests) {
this.windowSize = windowSizeMs;
this.subWindowCount = subWindowCount;
this.subWindowSize = windowSizeMs / subWindowCount;
this.maxRequests = maxRequests;
this.subWindows = new AtomicInteger[subWindowCount];
this.windowStarts = new long[subWindowCount];

long now = System.currentTimeMillis();
for (int i = 0; i < subWindowCount; i++) {
subWindows[i] = new AtomicInteger(0);
windowStarts[i] = now;
}
}

/**
* 尝试获取许可
*/
public boolean tryAcquire() {
long now = System.currentTimeMillis();
int currentIndex = (int) ((now / subWindowSize) % subWindowCount);

// 清理过期的小窗口数据
synchronized (this) {
if (now - windowStarts[currentIndex] >= subWindowSize) {
subWindows[currentIndex].set(0);
windowStarts[currentIndex] = now;
}
}

// 计算当前窗口内的总请求数
int totalRequests = 0;
for (int i = 0; i < subWindowCount; i++) {
if (now - windowStarts[i] < windowSize) {
totalRequests += subWindows[i].get();
}
}

// 判断是否超过阈值
if (totalRequests >= maxRequests) {
return false;
}

// 增加当前小窗口计数
subWindows[currentIndex].incrementAndGet();
return true;
}
}

2.2.3 优缺点分析

优点 缺点
平滑处理流量,避免临界突变 实现相对复杂
精度可调(通过小窗口数量) 内存占用随小窗口数量增加
更准确的流量控制 计算总请求数有一定开销

2.3 漏桶算法(Leaky Bucket)

漏桶算法以固定速率处理请求,无论流入速率如何变化,流出速率始终保持恒定。

2.3.1 原理

  • 想象一个底部有漏洞的水桶
  • 请求像水一样流入桶中
  • 水以固定速率从漏洞流出(被处理)
  • 桶满时,新流入的水(请求)被丢弃
1
2
3
4
5
6
7
请求流入(任意速率)

┌─────────┐
│ 漏桶 │ ← 桶容量固定
│ ~~~~~~~ │
└────┬────┘
↓ 固定速率流出(处理请求)

2.3.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 漏桶限流算法实现
*/
public class LeakyBucketRateLimiter {
// 桶的容量
private final int capacity;
// 漏水速率(每秒处理请求数)
private final int leakRate;
// 当前水量(待处理请求数)
private volatile double water;
// 上次漏水时间
private volatile long lastLeakTime;

public LeakyBucketRateLimiter(int capacity, int leakRatePerSecond) {
this.capacity = capacity;
this.leakRate = leakRatePerSecond;
this.water = 0;
this.lastLeakTime = System.currentTimeMillis();
}

/**
* 尝试获取许可
*/
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();

// 计算这段时间应该漏掉的水量
long elapsed = now - lastLeakTime;
double leaked = elapsed * leakRate / 1000.0;

// 更新桶中水量
water = Math.max(0, water - leaked);
lastLeakTime = now;

// 判断桶是否还有空间
if (water < capacity) {
water += 1;
return true;
}

// 桶已满,拒绝请求
return false;
}
}

2.3.3 优缺点分析

优点 缺点
流量绝对平滑,无突发 无法应对突发流量(削峰填谷)
保护下游服务稳定 可能降低系统吞吐量
实现相对简单 不适合需要突发能力的场景

2.4 令牌桶算法(Token Bucket)

令牌桶算法是业界最常用的限流算法,它允许一定程度的突发流量,同时保持长期平均速率稳定。

2.4.1 原理

  • 以固定速率向桶中放入令牌
  • 桶有最大容量限制
  • 请求需要获取令牌才能执行
  • 桶中有足够令牌时,可一次性获取多个(支持突发)
  • 桶空时,请求等待或拒绝
1
2
3
4
5
6
7
令牌以固定速率生成

┌─────────┐
│ 令牌桶 │ ← 桶容量固定
│ ○ ○ ○ ○ │
└────┬────┘
↓ 请求获取令牌后执行

2.4.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 令牌桶限流算法实现
*/
public class TokenBucketRateLimiter {
// 桶的容量
private final int capacity;
// 令牌生成速率(每秒)
private final int tokenRate;
// 当前令牌数
private volatile double tokens;
// 上次添加令牌时间
private volatile long lastAddTime;

public TokenBucketRateLimiter(int capacity, int tokenRatePerSecond) {
this.capacity = capacity;
this.tokenRate = tokenRatePerSecond;
this.tokens = capacity; // 初始满桶
this.lastAddTime = System.currentTimeMillis();
}

/**
* 尝试获取指定数量的令牌
* @param requestedTokens 请求的令牌数
* @return true: 获取成功, false: 获取失败
*/
public synchronized boolean tryAcquire(int requestedTokens) {
long now = System.currentTimeMillis();

// 计算这段时间应该添加的令牌数
long elapsed = now - lastAddTime;
double newTokens = elapsed * tokenRate / 1000.0;

// 更新令牌数(不超过容量)
tokens = Math.min(capacity, tokens + newTokens);
lastAddTime = now;

// 判断是否有足够令牌
if (tokens >= requestedTokens) {
tokens -= requestedTokens;
return true;
}

// 令牌不足
return false;
}

/**
* 尝试获取1个令牌
*/
public boolean tryAcquire() {
return tryAcquire(1);
}
}

2.4.3 优缺点分析

优点 缺点
支持突发流量(有令牌时可快速处理) 实现相对复杂
长期平均速率稳定 需要合理设置桶容量
灵活性强,应用场景广泛 突发流量可能冲击下游

三、算法对比与选型指南

3.1 核心特性对比

特性 计数器 滑动窗口 漏桶 令牌桶
实现复杂度 ⭐ 简单 ⭐⭐ 中等 ⭐⭐ 中等 ⭐⭐ 中等
平滑度 ⭐ 差 ⭐⭐⭐ 好 ⭐⭐⭐ 极好 ⭐⭐⭐ 好
突发支持 ❌ 不支持 ⚠️ 有限支持 ❌ 不支持 ✅ 支持
内存占用 ⭐ 极低 ⭐⭐ 中等 ⭐ 低 ⭐ 低
精度控制 ⭐ 低 ⭐⭐⭐ 高 ⭐⭐⭐ 高 ⭐⭐⭐ 高

3.2 选型建议

场景 推荐算法 理由
简单接口限流 计数器 实现简单,性能高
严格平滑流量 漏桶 输出绝对平滑
需要突发能力 令牌桶 允许突发,平均速率稳定
高精度限流 滑动窗口 避免临界突变
网关层限流 令牌桶 灵活应对各种流量模式
用户级限流 滑动窗口 公平性更好

四、分布式限流方案

单机限流无法满足分布式场景需求,以下是主流分布式限流方案:

4.1 Redis + Lua 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 令牌桶分布式限流 Lua 脚本
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local requested = tonumber(ARGV[3]) -- 请求令牌数
local now = tonumber(ARGV[4]) -- 当前时间戳

local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now

-- 计算新增令牌
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * rate / 1000)

-- 判断是否允许通过
local allowed = new_tokens >= requested
if allowed then
new_tokens = new_tokens - requested
end

-- 更新桶状态
redis.call('HMSET', key, 'tokens', new_tokens, 'last_time', now)
redis.call('EXPIRE', key, 60)

return allowed and 1 or 0

4.2 开源限流组件

组件 算法 特点
Guava RateLimiter 令牌桶 Google 出品,单机版
Sentinel 多种 阿里开源,功能全面
Hystrix 线程池/信号量 Netflix 开源,已停止维护
Resilience4j 多种 轻量级,函数式编程
Redis Cell 令牌桶 Redis 模块,高性能

五、生产环境最佳实践

5.1 限流参数配置原则

  1. 基于容量规划:根据系统压测结果设置阈值
  2. 渐进式调整:从宽松开始,逐步收紧
  3. 区分优先级:核心接口限流更严格
  4. 动态调整:结合监控数据自动调整

5.2 限流与降级、熔断的配合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
流量入口

┌─────────────┐
│ 限流层 │ ← 第一道防线,控制流量大小
└──────┬──────┘

┌─────────────┐
│ 熔断层 │ ← 第二道防线,故障快速失败
└──────┬──────┘

┌─────────────┐
│ 降级层 │ ← 第三道防线,保障核心功能
└──────┬──────┘

业务逻辑

5.3 监控与告警

  • 限流触发次数:了解系统压力情况
  • 限流成功率:评估限流策略有效性
  • 接口响应时间:验证限流是否缓解压力
  • 业务成功率:确保限流不影响核心业务

六、总结

限流是保障系统稳定性的重要手段,选择合适的限流算法需要综合考虑业务场景、性能要求和实现成本:

  • 计数器算法:适合简单场景,快速实现
  • 滑动窗口:适合需要精确控制的场景
  • 漏桶算法:适合严格平滑流量的场景
  • 令牌桶算法:适合大多数场景,尤其是需要支持突发流量的场景

在实际生产环境中,建议结合多种限流策略,配合降级、熔断等手段,构建完善的服务保护体系。


本文介绍了四种主流限流算法的原理、实现和选型建议。限流不仅是技术问题,更是对业务理解和系统架构设计的考验。希望本文能帮助你在实际项目中做出正确的限流决策。