漏桶和令牌桶有啥区别?
结论:漏桶(Leaky Bucket)以恒定速率放行,把突发流量整形为均匀流量;令牌桶(Token Bucket)以恒定速率产生令牌,桶里攒下的令牌允许一次性突发。要削峰稳压用漏桶,要限平均速率又容许突发用令牌桶——绝大多数接口限流场景选令牌桶。
一、核心差异
| 维度 | 漏桶 | 令牌桶 |
|---|---|---|
| 桶里装什么 | 待处理的请求 | 可消耗的令牌 |
| 输出速率 | 恒定 | 平均恒定,瞬时可变 |
| 突发支持 | 不支持,超出即排队或丢弃 | 支持,受桶容量限制 |
| 容量含义 | 可排队的请求数 | 可一次性消耗的令牌数 |
| 典型实现 | 队列加定时出队、Sentinel 匀速排队 | Guava RateLimiter、Redis 令牌桶 Lua |
| 适用场景 | 保护下游、流量整形 | 接口限流、允许短时突发 |
二、两张图看懂
漏桶:不管进水多猛,出水口恒定 令牌桶:令牌匀速滴入,桶满则溢出
┌───────────┐ ┌───────────┐
请求 │ ┌─────┐ │ 恒定速率 令牌 │ ● ● ● ● │
────►│ │ ███ │──┼──► 放行 ────►│ ● ● ● │
突发 │ │ ███ │ │ 生成 │ 容量上限 │
│ └─────┘ │ └─────┬─────┘
└───────────┘ │ 取到令牌才放行
桶满则丢弃或排队 ▼
请求 ──► 放行
一个常被忽略的等价关系:漏桶的恒定输出,正是令牌桶在桶空时的行为;令牌桶多出来的只是「预存额度」。
三、代码:令牌桶(Guava)
// 每秒 100 个令牌,预热 3 秒:冷启动时逐步放开,避免瞬间打满
private final RateLimiter limiter =
RateLimiter.create(100.0, 3, TimeUnit.SECONDS);
public boolean tryAcquire() {
return limiter.tryAcquire(); // 非阻塞,拿不到立即返回 false
}
public void acquireBlocking() {
limiter.acquire(); // 阻塞直到拿到令牌
}
// 允许突发:SmoothBursty 默认 maxBurstSeconds = 1.0,
// 即桶容量约等于 1 秒的令牌量
RateLimiter bursty = RateLimiter.create(100.0);
Guava 的 SmoothBursty 是令牌桶,SmoothWarmingUp 是带预热的令牌桶;它没有现成的漏桶实现,需要自己用队列做。
四、代码:漏桶(Redis 队列加定时消费)
-- KEYS[1] = queue:api ARGV[1] = 入队时间戳(ms) ARGV[2] = 容量
-- 入队前检查排队长度,控制的是「队尾丢弃」
local len = redis.call('LLEN', KEYS[1])
if len >= tonumber(ARGV[2]) then
return 0
end
redis.call('RPUSH', KEYS[1], ARGV[1])
return 1
消费端以固定间隔 LPOP 并转发,出队速率就是放行速率。Sentinel 的匀速排队(Rate Limiter)控制行为本质也是漏桶:请求按固定间隔排队通过,超过 maxQueueingTimeMs 直接拒绝。
五、常见追问
Q1:令牌桶能模拟漏桶吗?
不能完全等价。令牌桶在桶空时是恒定速率,但桶满时允许突发;把桶容量设为 1,就退化为「每 1/rate 秒放行一个」,接近漏桶,但缺少排队与平滑能力。
Q2:限流算法还有哪些? 固定窗口(最简单,有临界突刺)、滑动窗口(按时间片加权,精度高)、并发数限流(信号量,防慢调用堆积)、自适应限流(按 RT 与 CPU 动态调阈值)。
Q3:线上该选哪个? 对外 API 用令牌桶,允许用户短时突发,体验更好;保护脆弱下游(老数据库、第三方接口)用漏桶排队,让流量平滑;秒杀入口用滑动窗口加令牌桶组合。
Q4:桶容量怎么定? 容量决定能容忍多大的瞬时突发。经验值是「平均 QPS × 1 到 3 秒」;容量太大等于没限流,太小则用户点两下就被拒。
小结
- 漏桶整形,令牌桶限速兼突发,区别在桶里装的是请求还是令牌。
- 令牌桶是限流主力,漏桶适合保护下游与削峰。
- 桶容量决定了能容忍多大突发,这个参数比速率更值得调。