3. 令牌桶算法详解

令牌桶(Token Bucket)是业界应用最广泛的限流算法之一,尤其适合催收触达API这类需要容忍突发流量、同时保证长期平均速率的场景。本节从算法原理、参数配置、适用场景到Python伪代码实现,进行完整拆解。

3.1 算法原理

令牌桶的核心思想是:系统以恒定速率向一个桶中放入令牌,每个请求必须从桶中获取一个令牌才能被处理;如果桶中令牌不足,请求被拒绝或排队。桶的容量限制了最大突发流量。

关键机制: 令牌的生成速率决定长期平均处理能力,桶容量决定短时突发上限。两者独立配置,互不干扰。
令牌桶算法核心流程 令牌桶算法核心流程 令牌生成器 速率 r 个/秒 令牌桶 容量 = b 当前令牌数 (≤ b) API 请求 令牌 足够? 处理请求 拒绝/排队
催收场景映射: 令牌生成速率 r 对应“催收系统每秒最多处理的触达请求数”;桶容量 b 对应“短时突发时允许积压的最大请求数”。例如 r=100, b=200 表示长期平均 100 QPS,但允许瞬间 200 个请求并发。

3.2 参数配置与计算

令牌桶有两个核心参数,配置时需要结合催收触达API的SLA和上游系统容量进行推算。

参数 符号 含义 催收场景配置建议
令牌生成速率 r 每秒向桶中放入的令牌数,决定长期平均处理速率 取催收平台承诺的稳定QPS × 1.2 安全系数
桶容量 b 桶最多容纳的令牌数,决定最大突发流量 取上游系统(如短信网关)能承受的峰值QPS × 突发持续秒数

关键计算公式:

  • 最大突发请求数 = b(桶满时一次性放行)
  • 突发后恢复时间 = (b - 当前令牌数) / r
  • 实际吞吐量 ≤ min(请求到达率, r + 突发补偿)
配置陷阱: 桶容量 b 设置过大(如 r=100, b=10000),会导致下游系统被瞬时洪峰冲垮,失去限流保护意义。建议 b ≤ 2r ~ 5r,具体取决于下游抗压能力。

3.3 适用场景分析

令牌桶算法在催收触达API中具有独特优势,尤其适合以下三类场景:

  • 允许突发流量: 催收任务经常在整点批量触发(如上午10:00批量发送提醒),令牌桶允许短时超过平均速率,避免任务积压。
  • 长期速率可控: 即使突发,长期平均速率仍被 r 严格限制,不会压垮下游短信/语音通道。
  • 预热与冷启动: 桶初始为满,系统启动后即可处理突发请求,无需等待预热。

与其他限流算法的对比:

算法 是否支持突发 长期速率控制 实现复杂度 催收推荐度
令牌桶 ✅ 支持 严格 低 ⭐⭐⭐⭐⭐
漏桶 ❌ 不支持 严格 低 ⭐⭐⭐
滑动窗口 ⚠️ 有限支持 较严格 中 ⭐⭐⭐⭐
计数器 ❌ 不支持 宽松 最低 ⭐⭐

3.4 Python 伪代码实现

以下是一个生产级令牌桶的Python伪代码实现,包含时间戳驱动的令牌补充逻辑,避免使用定时器,性能更高。

import time

class TokenBucket:
    def __init__(self, rate: float, capacity: int):
        """
        :param rate: 令牌生成速率(个/秒)
        :param capacity: 桶容量(最大令牌数)
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity          # 初始桶满,允许突发
        self.last_refill_time = time.time()

    def _refill(self):
        """根据时间差补充令牌"""
        now = time.time()
        elapsed = now - self.last_refill_time
        # 计算这段时间应生成的令牌数
        new_tokens = elapsed * self.rate
        # 更新令牌数,不超过容量
        self.tokens = min(self.capacity, self.tokens + new_tokens)
        self.last_refill_time = now

    def try_acquire(self, tokens: int = 1) -> bool:
        """
        尝试获取 tokens 个令牌
        :return: True 表示获取成功,False 表示限流拒绝
        """
        self._refill()
        if self.tokens >= tokens:
            self.tokens -= tokens
            return True
        return False

    def get_available_tokens(self) -> float:
        """查看当前可用令牌数"""
        self._refill()
        return self.tokens

# ========== 催收场景使用示例 ==========
# 初始化:速率 100 QPS,容量 200(允许瞬间 200 并发)
bucket = TokenBucket(rate=100, capacity=200)

# 模拟请求
for i in range(250):
    if bucket.try_acquire():
        print(f"请求 {i+1}: 通过 ✅")
    else:
        print(f"请求 {i+1}: 限流 ❌")
    time.sleep(0.005)  # 模拟 5ms 间隔
实现要点:
  • 时间戳驱动: 每次请求时根据时间差计算令牌补充量,无需独立线程,避免竞态条件。
  • 初始桶满: 系统启动后立即允许突发,符合催收批量任务特征。
  • 浮点精度: 令牌数使用 float 类型,避免整数除法截断导致速率偏差。
令牌桶参数配置与流量关系 令牌桶参数配置与流量关系 时间 速率 r (平均) 突发流量区 请求 > r 消耗桶内令牌 平稳流量区 请求 ≈ r 令牌收支平衡 限流区 桶空 请求被拒绝 桶容量 b 决定突发上限:突发高度 ≤ b,突发宽度受速率 r 恢复约束
生产建议: 在催收触达API中,建议将令牌桶与动态速率调整结合——当下游返回限流或超时比例超过阈值时,自动降低 r 值,实现自适应保护。