4. 令牌桶限流算法原理

令牌桶(Token Bucket)是业界应用最广泛的限流算法之一,其核心思想是通过一个“桶”来平滑突发流量,同时保证长期平均速率可控。本章从数学模型出发,深入剖析令牌桶的工作机制,并与漏桶算法进行对比,帮助你在实际项目中做出正确的技术选型。

4.1 令牌桶算法数学模型

令牌桶算法包含三个核心参数:

  • 桶容量(b):桶中最多可存放的令牌数,决定了允许的突发流量上限。
  • 令牌生成速率(r):单位时间内向桶中添加的令牌数量,决定了长期平均速率。
  • 当前令牌数(c):桶中当前持有的令牌数量,初始值为桶容量 b。

算法执行流程如下:

  1. 系统以固定速率 r 向桶中添加令牌,若桶已满则丢弃多余令牌。
  2. 每个请求到达时,尝试从桶中获取 1 个(或多个)令牌。
  3. 若桶中令牌数 ≥ 请求所需令牌数,则放行请求,并从桶中扣除对应令牌。
  4. 若桶中令牌数不足,则拒绝请求或等待令牌补充。
关键公式: 在时间间隔 Δt 内,桶中令牌数的变化量为:
c(t+Δt) = min(b, c(t) + r × Δt - consumed)
其中 consumed 为 Δt 内消耗的令牌总数。
实践要点: 令牌桶的“突发”能力由桶容量 b 决定。例如,设置 r=100 req/s、b=200,则系统允许瞬间 200 个请求并发通过,但长期平均速率仍被限制在 100 req/s。

4.2 令牌桶 vs 漏桶算法对比

漏桶(Leaky Bucket)算法与令牌桶常被混淆,但二者在流量整形逻辑上存在本质差异。下表从多个维度进行对比:

对比维度 令牌桶算法 漏桶算法
核心机制 以固定速率生成令牌,请求消耗令牌 以固定速率漏水,请求注入桶中
突发处理 允许突发(桶容量内) 严格平滑,不允许突发
长期速率 严格等于令牌生成速率 r 严格等于漏水速率
流量整形效果 允许短时超速,但平均速率可控 完全平滑,输出流量恒定
实现复杂度 中等(需定时器或时间戳计算) 较低(队列 + 定时器)
典型应用 API 限流、微服务网关 网络流量整形、数据流控
避坑提示: 不要将令牌桶与漏桶混用。如果业务需要容忍突发流量(如秒杀场景),请选择令牌桶;如果业务要求绝对平滑的输出速率(如视频流推送),请选择漏桶。

4.3 令牌桶算法工作流程(SVG)

下图展示了令牌桶算法的完整工作流程,包括令牌生成、请求到达、令牌消耗与拒绝等关键环节:

令牌桶算法工作流程 令牌桶算法工作流程 令牌生成器 速率 r = 100/s 令牌桶 容量 b = 200 当前令牌数 c = 5 请求到达 c ≥ 1? 放行请求 拒绝请求 添加令牌 查询令牌数 是 否 扣减令牌 说明: • 令牌生成器以固定速率 r 向桶中添加令牌 • 桶满时多余令牌被丢弃 • 请求消耗令牌,不足时被拒绝

4.4 适用场景分析

不同的限流算法适用于不同的业务场景。以下是令牌桶与漏桶的典型适用场景对比:

场景类型 推荐算法 原因
API 网关限流 令牌桶 允许突发请求,用户体验好;长期速率可控
秒杀/抢购系统 令牌桶 瞬间高并发需要突发能力,桶容量可缓冲流量
视频流推送 漏桶 需要恒定码率输出,避免网络抖动
数据库连接池 令牌桶 允许短时连接峰值,保护数据库不被压垮
日志采集系统 漏桶 需要平滑写入,避免后端存储过载
微服务熔断降级 令牌桶 配合熔断器实现快速失败与恢复
选型建议: 在 Go 微服务架构中,90% 的限流场景推荐使用令牌桶算法。Go 标准库 golang.org/x/time/rate 提供了高效的令牌桶实现,可直接用于生产环境。漏桶算法更适合网络层流量整形,在应用层限流中使用较少。

4.5 令牌桶与漏桶对比(SVG)

下图从流量整形效果的角度,直观对比令牌桶与漏桶在处理突发流量时的差异:

令牌桶 vs 漏桶流量对比 令牌桶 vs 漏桶:突发流量处理对比 令牌桶 突发输入 令牌桶 容量 b=200 平滑输出 • 允许突发流量进入桶中 • 桶容量吸收突发峰值 • 输出速率 = 令牌生成速率 • 突发后输出仍保持平滑 漏桶 突发输入 漏桶 固定漏水速率 恒定输出 • 突发流量被强制排队 • 超出桶容量的请求被丢弃 • 输出速率严格恒定 • 无法利用突发能力 总结:令牌桶允许突发,漏桶强制平滑;选型取决于业务是否需要突发能力

4.6 Go 语言实现示例

Go 标准库 golang.org/x/time/rate 提供了高效的令牌桶实现。以下是一个简单的使用示例:

package main

import (
    "fmt"
    "time"
    "golang.org/x/time/rate"
)

func main() {
    // 创建令牌桶:每秒生成 10 个令牌,桶容量为 20
    limiter := rate.NewLimiter(10, 20)

    // 模拟 30 个请求
    for i := 0; i < 30; i++ {
        if limiter.Allow() {
            fmt.Printf("请求 %d 放行\n", i)
        } else {
            fmt.Printf("请求 %d 被限流\n", i)
        }
        time.Sleep(50 * time.Millisecond)
    }
}
代码说明: rate.NewLimiter(r, b) 创建令牌桶,r 为每秒令牌生成数,b 为桶容量。Allow() 方法尝试获取一个令牌,返回 true 表示放行,false 表示限流。如需等待令牌,可使用 Wait() 方法。

4.7 本章小结

  • 令牌桶算法通过“桶容量”和“令牌生成速率”两个参数,实现了允许突发且长期速率可控的限流效果。
  • 漏桶算法强制输出速率恒定,适合需要严格平滑流量的场景。
  • 在 Go 微服务架构中,golang.org/x/time/rate 是首选的令牌桶实现,性能优异且 API 简洁。
  • 选型时需根据业务是否允许突发流量来决定使用令牌桶还是漏桶。