2. 令牌桶算法原理

令牌桶算法(Token Bucket)是限流领域最经典的算法之一,也是 Go 生态中 rate 包、uber-go/ratelimit 等主流限流库的底层核心。理解它的工作机制、参数含义和适用边界,是构建限流骨架的第一步。

2.1 工作机制

令牌桶的核心思想是:系统以恒定速率向一个桶中放入令牌,每个请求必须从桶中获取一个令牌才能被处理。桶有最大容量,超出容量的令牌被丢弃;当桶为空时,请求被拒绝或等待。

关键行为:
  • 填充(Refill):后台定时器或事件驱动,以固定速率向桶中添加令牌。
  • 消费(Consume):请求到达时,从桶中取出一个或多个令牌。
  • 突发(Burst):桶容量允许短时间内的流量突发,只要桶中有积攒的令牌。
  • 丢弃(Drop):桶满时新令牌被丢弃;桶空时请求被拒绝。
令牌桶算法核心流程 令牌桶算法核心流程 令牌生成器 速率 r 个/秒 放入令牌 令牌桶 容量 = C 当前令牌数 消费令牌 请求处理 请求拒绝 有令牌? 否 是 💡 算法要点 • 令牌生成速率 r 决定长期平均速率 • 桶容量 C 决定最大突发流量 • 请求到达时桶中有令牌则通过,否则拒绝或等待

2.2 关键参数

令牌桶算法由两个核心参数驱动,它们共同决定了限流行为的“刚”与“柔”:

参数 符号 含义 典型值 影响
速率(Rate) r 令牌放入桶中的恒定速率,单位:个/秒 100 req/s 决定长期平均吞吐上限
容量(Capacity) C 桶最多能容纳的令牌数量 200 决定最大突发流量大小
参数关系速记: 速率 r 控制“平均速度”,容量 C 控制“爆发力”。如果 C = r,则退化为漏桶算法,完全不允许突发。

2.3 适用场景

令牌桶算法并非万能,它在以下场景中表现优异,但在另一些场景中需要谨慎使用:

✅ 推荐场景

  • API 网关限流:允许客户端在空闲时段积攒令牌,应对突发请求。
  • 微服务间调用保护:防止下游服务被瞬时流量冲垮,同时保留一定的弹性。
  • 消息队列消费速率控制:以恒定速率拉取消息,但允许积压时快速消费。
  • 数据库连接池限流:控制每秒建立的连接数,避免数据库过载。

⚠️ 慎用场景

  • 严格公平性要求:令牌桶不区分请求优先级,所有请求平等竞争令牌。
  • 精确速率控制:如果要求每秒严格不超过 r 个请求(无突发),应使用漏桶算法。
  • 分布式全局限流:单机令牌桶无法直接用于分布式环境,需要配合分布式协调(如 Redis 实现)。
令牌桶 vs 漏桶 场景对比 令牌桶 vs 漏桶:场景选择 令牌桶 允许突发 平均速率可控 空闲积攒令牌 非严格公平 适用:API网关、微服务调用 消息队列消费、连接池 漏桶 严格匀速 无突发 浪费空闲容量 公平性好 适用:严格速率限制 流量整形、网络 QoS 选择

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 退化为漏桶,无突发
调优口诀: 速率定平均,容量定爆发;容量越大,限流越“软”;容量等于速率,限流最“硬”。