3、令牌桶算法原理

令牌桶算法(Token Bucket)是限流领域最常用的算法之一,它通过一个“桶”来平滑突发流量,同时允许一定程度的突发请求,比漏桶算法更具灵活性。本节将深入剖析令牌桶的工作机制、核心参数、适用场景,并与漏桶算法进行对比。

3.1 工作机制

令牌桶算法的核心思想是:系统以恒定速率向一个桶中放入令牌,每个请求在到达时必须从桶中获取一个令牌才能被处理。如果桶中没有令牌,请求将被拒绝或等待。

  • 令牌生成:系统以固定的速率(如每秒 100 个)向桶中添加令牌。
  • 桶容量:桶有一个最大容量(如 200 个),超过容量的令牌会被丢弃。
  • 请求消费:每个请求到达时,尝试从桶中取出一个令牌。如果成功,请求被放行;如果失败,请求被限流。
  • 突发处理:当桶中积累了大量令牌(如空闲期),可以一次性处理多个突发请求,直到桶空。
关键区别:令牌桶允许突发流量,而漏桶算法强制平滑流量。这是两者最本质的差异。
令牌桶算法工作流程 令牌生成器 速率: 100/s 令牌桶 (容量: 200) T T T T T T T T T T T T 请求到达 有令牌? 放行 拒绝 说明 • 令牌按固定速率生成 • 桶满则丢弃多余令牌 • 请求消耗令牌后放行 • 无令牌则拒绝请求

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 限流、微服务调用、突发业务 流量整形、网络带宽控制、严格平滑
缺点 突发可能压垮下游 无法应对突发,可能导致请求超时
选型提醒:如果业务允许短时间内的流量尖峰(如电商秒杀前的预热),令牌桶是更好的选择。如果业务要求绝对的流量平滑(如视频流传输),漏桶算法更合适。
令牌桶 vs 漏桶:流量处理对比 令牌桶算法 输入流量 令牌桶 burst=200 输出流量 允许突发尖峰 漏桶算法 输入流量 漏桶 固定速率 输出流量 强制平滑输出 核心差异总结 • 令牌桶:允许突发,适合 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
}
实现要点:
• 使用 time.Now() 计算时间差,避免额外定时器开销。
• 每次请求时先补充令牌,再判断是否放行。
• 使用 sync.Mutex 保证并发安全。
• 初始令牌数设为 burst,确保系统启动时能处理突发请求。