2. 令牌桶算法原理
令牌桶算法(Token Bucket)是限流领域最经典的算法之一,也是 Go 生态中 rate 包、uber-go/ratelimit 等主流限流库的底层核心。理解它的工作机制、参数含义和适用边界,是构建限流骨架的第一步。
2.1 工作机制
令牌桶的核心思想是:系统以恒定速率向一个桶中放入令牌,每个请求必须从桶中获取一个令牌才能被处理。桶有最大容量,超出容量的令牌被丢弃;当桶为空时,请求被拒绝或等待。
关键行为:
- 填充(Refill):后台定时器或事件驱动,以固定速率向桶中添加令牌。
- 消费(Consume):请求到达时,从桶中取出一个或多个令牌。
- 突发(Burst):桶容量允许短时间内的流量突发,只要桶中有积攒的令牌。
- 丢弃(Drop):桶满时新令牌被丢弃;桶空时请求被拒绝。
2.2 关键参数
令牌桶算法由两个核心参数驱动,它们共同决定了限流行为的“刚”与“柔”:
| 参数 | 符号 | 含义 | 典型值 | 影响 |
|---|---|---|---|---|
| 速率(Rate) | r |
令牌放入桶中的恒定速率,单位:个/秒 | 100 req/s | 决定长期平均吞吐上限 |
| 容量(Capacity) | C |
桶最多能容纳的令牌数量 | 200 | 决定最大突发流量大小 |
参数关系速记: 速率 r 控制“平均速度”,容量 C 控制“爆发力”。如果 C = r,则退化为漏桶算法,完全不允许突发。
2.3 适用场景
令牌桶算法并非万能,它在以下场景中表现优异,但在另一些场景中需要谨慎使用:
✅ 推荐场景
- API 网关限流:允许客户端在空闲时段积攒令牌,应对突发请求。
- 微服务间调用保护:防止下游服务被瞬时流量冲垮,同时保留一定的弹性。
- 消息队列消费速率控制:以恒定速率拉取消息,但允许积压时快速消费。
- 数据库连接池限流:控制每秒建立的连接数,避免数据库过载。
⚠️ 慎用场景
- 严格公平性要求:令牌桶不区分请求优先级,所有请求平等竞争令牌。
- 精确速率控制:如果要求每秒严格不超过 r 个请求(无突发),应使用漏桶算法。
- 分布式全局限流:单机令牌桶无法直接用于分布式环境,需要配合分布式协调(如 Redis 实现)。
2.4 Go 语言中的令牌桶实现
Go 标准库 golang.org/x/time/rate 提供了官方令牌桶实现,核心 API 如下:
import "golang.org/x/time/rate"
// 创建一个速率 100 req/s、容量 200 的令牌桶
limiter := rate.NewLimiter(100, 200)
// 阻塞等待,直到获取到令牌
err := limiter.Wait(ctx)
// 非阻塞尝试获取令牌
if limiter.Allow() {
// 处理请求
} else {
// 拒绝请求
}
// 预留令牌,返回 Reservation 对象
reserve := limiter.Reserve()
if reserve.OK() {
time.Sleep(reserve.Delay())
// 处理请求
}
⚠️ 生产环境注意:
Wait方法在上下文取消时会返回错误,务必处理超时场景。Allow方法内部调用ReserveN(time.Now(), 1),如果桶为空则立即返回 false。- 令牌桶的突发能力由容量参数控制,不要将容量设置过大,否则限流效果会减弱。
2.5 参数调优建议
| 业务场景 | 速率 r | 容量 C | 说明 |
|---|---|---|---|
| 高并发 API | 峰值 QPS × 1.2 | r × 2 | 允许短时 2 倍突发 |
| 数据库连接池 | 连接池大小 / 平均响应时间 | r × 1.5 | 防止连接风暴 |
| 消息消费 | 下游处理能力 | r × 3 ~ r × 5 | 允许积压时快速消费 |
| 严格限流 | 目标 QPS | r | 退化为漏桶,无突发 |
调优口诀: 速率定平均,容量定爆发;容量越大,限流越“软”;容量等于速率,限流最“硬”。