我来为你详细讲解令牌桶算法(Token Bucket),这是限流领域的核心算法,面试高频考点。
想象一个水桶(桶容量固定),以恒定速率往里面放令牌(token)。请求来了需要拿到令牌才能执行,拿不到就被限流。
令牌以固定速率生成
↓
┌─────────────────┐
│ 令牌桶 │ ←── 桶容量固定(如容量=10)
│ [○][○][○][○] │ 当前令牌数 ≤ 容量
└────────┬────────┘
↓ 请求尝试获取令牌
┌─────────────────┐
│ 请求处理 │ ←── 拿到令牌:通过
│ 请求被拒绝 │ ←── 没令牌:限流/等待
└─────────────────┘参数 | 含义 | 示例 |
|---|---|---|
容量(Capacity) | 桶最多存多少令牌 | 100 tokens |
产生速率(Rate) | 每秒/每毫秒产生令牌数 | 10 tokens/sec |
当前令牌数 | 实时可用令牌 | 动态变化 |
class TokenBucket {
long capacity; // 桶容量
long tokens; // 当前令牌数
long lastTime; // 上次更新时间
long rate; // 产生速率(令牌/毫秒)
synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 1. 计算这段时间产生的令牌数,并加到桶中(不超过容量)
tokens = Math.min(capacity, tokens + (now - lastTime) * rate);
lastTime = now;
// 2. 尝试获取令牌
if (tokens > 0) {
tokens--;
return true; // 获取成功,请求通过
}
return false; // 获取失败,被限流
}
}特性 | 说明 | 意义 |
|---|---|---|
允许突发流量 | 桶内有存量令牌时,可一次性取走 | 应对正常业务尖峰(非恶意攻击) |
长期速率恒定 | 产生速率固定,限制平均流量 | 保护系统不被持续高流量压垮 |
平滑限流 | 流量曲线平滑,非断崖式拒绝 | 用户体验更好 |
假设:容量=10,速率=1 token/sec
时间线:
t0: 桶满10个令牌,突然来了15个请求
→ 前10个请求瞬间通过(突发)
→ 后5个请求按1个/秒匀速通过
→ 长期平均速率 = 1 req/sec对比维度 | 令牌桶 Token Bucket | 漏桶 Leaky Bucket |
|---|---|---|
核心思想 | 令牌匀速产生,请求取令牌 | 请求匀速流出,桶缓冲请求 |
流量形状 | 允许突发,长期匀速 | 强制匀速,无突发 |
桶内存储 | 存储令牌(许可) | 存储请求(数据) |
突发流量 | ✅ 支持 | ❌ 不支持 |
实现复杂度 | 简单(只需记录令牌数) | 稍复杂(需要队列+定时消费) |
典型应用 | 网络流量控制、API限流 | 流量整形(Traffic Shaping) |
漏桶示意图(强制匀速):
请求流入 → [ 漏桶/队列 ] → 匀速流出(固定速率处理)
↑
桶满则溢出(拒绝)面试金句:令牌桶限制的是平均速率,允许突发;漏桶限制的是瞬时流速,强制匀速。
public class TokenBucket {
private final long capacity; // 桶容量
private final long rate; // 每秒产生令牌数
private double tokens; // 当前令牌数(用double支持小数)
private long lastTimestamp; // 上次更新时间(秒)
public TokenBucket(long capacity, long rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = capacity; // 初始满桶
this.lastTimestamp = System.currentTimeMillis() / 1000;
}
public synchronized boolean allowRequest() {
long now = System.currentTimeMillis() / 1000;
// 计算新产生的令牌
tokens = Math.min(capacity, tokens + (now - lastTimestamp) * rate);
lastTimestamp = now;
if (tokens >= 1) {
tokens--;
return true;
}
return false;
}
}import com.google.common.util.concurrent.RateLimiter;
// 创建:每秒产生2个令牌(平滑突发)
RateLimiter limiter = RateLimiter.create(2.0);
// 阻塞获取(拿不到就等待)
limiter.acquire();
// 非阻塞尝试(立即返回是否成功)
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 被限流,返回503或降级
}-- 令牌桶限流 Lua 脚本(原子操作)
local key = KEYS[1] -- 桶的key
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4]) -- 需要令牌数
local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)
-- 获取当前状态
local last_tokens = redis.call('get', key .. ':tokens')
local last_updated = redis.call('get', key .. ':last_updated')
if last_tokens == false then
last_tokens = capacity
last_updated = 0
else
last_tokens = tonumber(last_tokens)
last_updated = tonumber(last_updated)
end
-- 计算新令牌数
local delta = math.max(0, now - last_updated)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
-- 更新状态
redis.call('setex', key .. ':tokens', ttl, new_tokens)
redis.call('setex', key .. ':last_updated', ttl, now)
return allowed and 1 or 0场景 | 配置建议 | 原因 |
|---|---|---|
API网关限流 | 容量=100,速率=1000/s | 应对正常流量尖峰 |
用户级限流 | 容量=20,速率=10/s | 防单个用户刷接口 |
下载限速 | 漏桶更合适 | 需要严格匀速 |
秒杀系统 | 令牌桶+队列 | 允许瞬间高并发,保护后端 |
Q1:令牌桶的"桶"具体指什么?是队列吗?
不是队列!只是一个计数器(记录当前可用令牌数)。不需要存储具体请求,这是和漏桶的本质区别。
Q2:令牌桶会有"精度"问题吗?
有。如果更新间隔太长,计算的令牌数可能不准确。生产环境建议:
Q3:Guava的RateLimiter是令牌桶吗?
是,但做了优化。它使用平滑预热(SmoothWarmingUp)和平滑突发(SmoothBursty)两种模式,基于令牌桶思想但实现更高效(使用
resync算法避免定时任务)。
Q4:令牌桶容量设置多大合适?
令牌桶 = 匀速生产 + 桶内缓存 + 突发消耗 + 长期限速。既保护系统,又兼顾用户体验,是互联网限流的首选算法。