一、限流算法概述
限流算法用于控制系统的请求处理速率,防止系统因突发流量而崩溃。主要目标是:
-
保护系统:防止突发流量(如恶意攻击、用户激增)导致系统资源(CPU、内存、数据库连接)耗尽而崩溃。
-
平滑流量:削峰填谷,将不规则的请求流量整形为比较均匀的流量,保护下游服务。
-
保证公平性:合理分配请求处理机会
-
保证服务质量:为重要业务或用户提供有保障的请求速率,实现服务的"优雅降级"。
令牌桶和漏桶是两种最经典、应用最广泛的算法,本文将对两者进行详细的对比介绍。
二、令牌桶算法(Token Bucket)
1. 核心思想
想象一个桶,以固定的速率向里面放入"令牌"。每个请求需要获取并消耗一个(或多个)令牌才能被处理。令牌桶有容量限制,当桶满时,新令牌被丢弃。
2. 工作原理
-
桶 :一个容量为
burst(C)的容器,用于存放令牌。 -
生成速率 :以固定的速率
rate(r)(如 10个/秒)向桶中添加令牌。 -
消耗:请求到达时,从桶中取出所需数量的令牌(通常为1个)。如果桶中有足够的令牌,则请求被放行;如果令牌不足,则请求被限流(拒绝或等待)。
python
- 算法参数:
- 桶容量(burst capacity):C
- 令牌添加速率:r tokens/sec
- 当前令牌数:tokens
3. 工作流程

text
初始化:tokens = C
处理请求时:
1. 计算自上次更新到当前时间应添加的令牌数
new_tokens = (当前时间 - 上次更新时间) × r
2. 更新令牌数(不超过桶容量)
tokens = min(C, tokens + new_tokens)
3. 如果 tokens ≥ 1:
tokens -= 1
允许请求通过
否则:
拒绝请求
4. 代码实现示例
python
python
import time
class TokenBucket:
def __init__(self, capacity, fill_rate):
self.capacity = float(capacity) # 桶容量
self.tokens = float(capacity) # 当前令牌数
self.fill_rate = float(fill_rate) # 添加速率
self.last_time = time.time()
def consume(self, tokens=1):
"""尝试消费指定数量的令牌"""
if tokens <= self._get_tokens():
self.tokens -= tokens
return True
return False
def _get_tokens(self):
now = time.time()
if self.tokens < self.capacity:
# 计算新增令牌
delta = self.fill_rate * (now - self.last_time)
self.tokens = min(self.capacity, self.tokens + delta)
self.last_time = now
return self.tokens
5. 关键特性
-
允许突发流量:这是令牌桶最显著的特点。如果桶是满的(比如容量100),那么瞬间可以处理最多100个请求,这对应流量的"突发峰值"。
-
长期平均速率受限 :长期的请求处理速率会被限制在
rate。 -
灵活性 :可以通过调整
burst(桶容量)和rate(生成速率)来平衡突发处理能力和平均速率。
三、漏桶算法(Leaky Bucket)
1. 核心思想
想象一个底部有固定大小出水口的漏桶。请求像水一样以任意速率流入桶中。桶以恒定的速率(出水口大小)向外漏出请求进行处理。如果水流入过快,桶会蓄水;当水超过桶容量时,多余的水(请求)会溢出(被拒绝)。
2. 工作原理
-
桶 :一个容量为
capacity(C)的队列(FIFO)。 -
流入速率:请求可以以任意速率到达并进入队列(如果队列未满)。
-
流出速率 :以固定的速率
rate(r)从队列头部取出请求进行处理。 -
溢出:当队列满时,新到达的请求被丢弃或拒绝。
python
- 算法参数:
- 桶容量:C
- 流出速率:r requests/sec
- 当前水量:water
3. 工作流程

python
text
初始化:water = 0, last_leak_time = now()
处理请求时:
1. 先漏水:计算自上次漏水到当前时间应流出的水量
leak_amount = (当前时间 - last_leak_time) × r
water = max(0, water - leak_amount)
2. 如果 water + 1 ≤ C:
water += 1
请求进入队列等待处理
否则:
拒绝请求
4. 代码实现示例
python
python
import time
class LeakyBucket:
def __init__(self, capacity, leak_rate):
self.capacity = float(capacity) # 桶容量
self.water = 0.0 # 当前水量
self.leak_rate = float(leak_rate) # 漏水速率
self.last_leak_time = time.time()
def allow_request(self):
now = time.time()
# 1. 先漏水
leak_amount = self.leak_rate * (now - self.last_leak_time)
self.water = max(0.0, self.water - leak_amount)
self.last_leak_time = now
# 2. 检查是否能加水
if self.water + 1 <= self.capacity:
self.water += 1
return True
return False
5. 关键特性
-
流量绝对平滑 :无论输入流量多么不规则,流出速率始终是恒定的
rate。这是与令牌桶最大的区别。 -
不允许突发:即使短时间内来大量请求,处理速率也保持不变,多余的请求需要在队列中等待。突发流量会被"削峰填谷"。
-
强制恒定的输出间隔 :每个请求处理的时间间隔严格为
1/rate。
四、两种算法对比
| 特性 | 令牌桶 (Token Bucket) | 漏桶 (Leaky Bucket) |
|---|---|---|
| 核心功能 | 控制平均流入速率,允许突发 | 控制恒定流出速率,平滑流量 |
| 流量模型 | 允许一定程度的突发流量(≤桶容量) | 输出流量绝对平滑,无突发 |
| 关键参数 | 容量(burst) + 生成速率(rate) |
容量(capacity) + 流出速率(rate) |
| 实现容器 | 计数器(当前令牌数) | 队列(FIFO) |
| 请求处理 | 消耗令牌,立即处理 | 进入队列,等待按固定速率处理 |
| 能否应对突发 | 可以(取决于桶容量) | 不可以(流出速率恒定) |
| 主要目的 | 限制请求的平均速率,同时具备一定弹性 | 确保请求以恒定的速率被处理 |
| 实现复杂度 | 中等 | 相对简单 |
| 内存需求 | 需要存储令牌数 | 需要存储水量 |
| 适用场景 | 需要应对突发流量 | 需要严格平滑流量 |
| 典型应用 | API限流、网络流量控制 | 网络包整形、恒定速率处理 |
| 类比 | 游乐园售票处:每小时卖100张票(平均速率),但一开门可以把预先准备好的50张票立刻卖光(突发)。 | 老式水龙头下的水桶:无论你如何倒水,水流出的速度都是一样的。 |
五、实际应用考虑
1. 在实际应用中:
-
令牌桶更常用 :因为它更符合互联网业务场景。用户行为本身具有突发性(如刷新页面),系统在能承受的范围内应该允许合理的突发,以提升用户体验。
Guava的RateLimiter、Nginx的limit_req模块(burst参数)本质上都是令牌桶或它的变种。 -
漏桶适用于特殊场景:当你必须保证下游服务接收到的请求是绝对匀速的,或者需要严格进行流量整形时。
-
分布式限流:在生产环境中,限流通常是分布式的(多个服务实例)。此时需要将桶的状态(如剩余令牌数)存储到外部集中式缓存(如 Redis)中,并借助 Lua 脚本等保证操作的原子性。
-
结合使用:有些系统会结合两种思想,例如先通过令牌桶允许一定突发,再通过漏桶进行最终平滑。
2. 分布式环境下的限流
python
python
# 使用Redis实现分布式令牌桶
import redis
import time
class DistributedTokenBucket:
def __init__(self, redis_client, key, capacity, fill_rate):
self.redis = redis_client
self.key = key
self.capacity = capacity
self.fill_rate = fill_rate
def consume(self, tokens=1):
lua_script = """
local key = KEYS[1]
local tokens = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local fill_rate = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local current_tokens = tonumber(bucket[1] or capacity)
local last_time = tonumber(bucket[2] or now)
-- 计算新增令牌
local delta = fill_rate * (now - last_time)
current_tokens = math.min(capacity, current_tokens + delta)
if current_tokens >= tokens then
current_tokens = current_tokens - tokens
redis.call('HMSET', key, 'tokens', current_tokens, 'last_time', now)
return 1
else
return 0
end
"""
now = time.time()
return bool(self.redis.eval(lua_script, 1, self.key,
tokens, self.capacity,
self.fill_rate, now))
3. 参数调优建议
-
监控指标:请求成功率、延迟、桶使用率
-
动态调整:根据系统负载动态调整速率
-
多级限流:结合应用层、服务层多层次限流
六、选择建议
选择令牌桶当:
-
需要应对合理范围内的突发流量
-
希望充分利用系统资源
-
例如:API网关限流、用户操作频率限制
选择漏桶当:
-
需要严格恒定输出速率
-
下游系统处理能力固定
-
例如:消息队列消费、数据库写入控制
混合使用:
在实际系统中,常常组合使用:
-
外层用令牌桶应对突发
-
内层用漏桶保证恒定处理速率
-
结合滑动窗口做更精细的控制
总结:
令牌桶和漏桶都是经典的流量控制算法,各有优劣。令牌桶更适合需要应对突发流量的场景,而漏桶更适合需要严格控制输出速率的场景。选择哪种取决于具体场景对突发流量的容忍度和对输出稳定性的要求。现代系统中常结合多种算法实现多层次的流量控制策略。