3、令牌桶算法原理
令牌桶算法(Token Bucket)是限流领域最常用的算法之一,它通过一个“桶”来平滑突发流量,同时允许一定程度的突发请求,比漏桶算法更具灵活性。本节将深入剖析令牌桶的工作机制、核心参数、适用场景,并与漏桶算法进行对比。
3.1 工作机制
令牌桶算法的核心思想是:系统以恒定速率向一个桶中放入令牌,每个请求在到达时必须从桶中获取一个令牌才能被处理。如果桶中没有令牌,请求将被拒绝或等待。
- 令牌生成:系统以固定的速率(如每秒 100 个)向桶中添加令牌。
- 桶容量:桶有一个最大容量(如 200 个),超过容量的令牌会被丢弃。
- 请求消费:每个请求到达时,尝试从桶中取出一个令牌。如果成功,请求被放行;如果失败,请求被限流。
- 突发处理:当桶中积累了大量令牌(如空闲期),可以一次性处理多个突发请求,直到桶空。
关键区别:令牌桶允许突发流量,而漏桶算法强制平滑流量。这是两者最本质的差异。
3.2 参数配置
令牌桶算法有两个核心参数,它们共同决定了限流的效果:
| 参数 | 含义 | 典型值 | 影响 |
|---|---|---|---|
| rate | 令牌生成速率(每秒放入桶中的令牌数) | 100/s, 1000/s | 决定长期平均请求速率上限 |
| burst | 桶的最大容量(允许的最大突发请求数) | 200, 500 | 决定短期突发流量的大小 |
配置建议:burst 值通常设置为 rate 的 1~5 倍。例如,rate=100/s 时,burst 可设为 200~500。burst 越大,系统允许的突发能力越强,但对下游的压力也越大。
3.3 适用场景
令牌桶算法特别适合以下场景:
- API 网关限流:允许用户在短时间内发起突发请求(如刷新页面),但长期速率受控。
- 微服务间调用:保护下游服务不被突发流量冲垮,同时允许正常的业务峰值。
- 消息队列消费:控制消费者从队列中拉取消息的速率,同时允许积压消息的快速消费。
- 数据库连接池:限制每秒的数据库查询次数,但允许短时间内的查询爆发。
3.4 与漏桶算法的对比
漏桶算法(Leaky Bucket)是另一种经典的限流算法,它与令牌桶算法在机制和效果上有显著差异。下表从多个维度进行对比:
| 对比维度 | 令牌桶算法 | 漏桶算法 |
|---|---|---|
| 核心机制 | 以固定速率生成令牌,请求消耗令牌 | 请求以固定速率从桶中漏出 |
| 突发处理 | 允许突发(桶中积累的令牌可一次性使用) | 强制平滑(请求必须排队以固定速率流出) |
| 流量形状 | 允许短时尖峰,长期平滑 | 完全平滑,无尖峰 |
| 实现复杂度 | 中等(需要定时器或时间戳计算) | 较低(队列 + 定时器) |
| 适用场景 | API 限流、微服务调用、突发业务 | 流量整形、网络带宽控制、严格平滑 |
| 缺点 | 突发可能压垮下游 | 无法应对突发,可能导致请求超时 |
选型提醒:如果业务允许短时间内的流量尖峰(如电商秒杀前的预热),令牌桶是更好的选择。如果业务要求绝对的流量平滑(如视频流传输),漏桶算法更合适。
3.5 Go 语言实现示例
以下是一个基于时间戳计算的令牌桶实现,不依赖外部定时器,适合在 Go 服务中直接使用:
package tokenbucket
import (
"sync"
"time"
)
type TokenBucket struct {
rate float64 // 每秒生成的令牌数
burst float64 // 桶的最大容量
tokens float64 // 当前令牌数
lastTime time.Time // 上次更新时间
mu sync.Mutex
}
func NewTokenBucket(rate, burst float64) *TokenBucket {
return &TokenBucket{
rate: rate,
burst: burst,
tokens: burst, // 初始时桶是满的
lastTime: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
// 计算时间差,补充令牌
elapsed := now.Sub(tb.lastTime).Seconds()
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.burst {
tb.tokens = tb.burst
}
tb.lastTime = now
// 判断是否有令牌可用
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
实现要点:
• 使用
• 每次请求时先补充令牌,再判断是否放行。
• 使用
• 初始令牌数设为 burst,确保系统启动时能处理突发请求。
• 使用
time.Now() 计算时间差,避免额外定时器开销。• 每次请求时先补充令牌,再判断是否放行。
• 使用
sync.Mutex 保证并发安全。• 初始令牌数设为 burst,确保系统启动时能处理突发请求。