2. 令牌桶算法原理
令牌桶(Token Bucket)是限流与流量整形领域最经典的算法之一,也是 Redis 令牌桶实现的理论基础。理解其核心参数与数学模型,是后续编写 Lua 脚本与熔断策略的前提。
2.1 核心参数
| 参数 | 符号 | 说明 | 典型值(示例) |
|---|---|---|---|
| 令牌生成速率 | r | 每秒钟向桶内放入的令牌数量,决定长期平均请求速率 | 100 个/秒 |
| 桶容量 | b | 桶最多能存放的令牌数,决定短时突发上限 | 200 个 |
| 当前令牌数 | c | 桶内实时可用令牌数量,随时间动态变化 | 0 ~ b |
| 最后填充时间 | t_last | 上次更新令牌数的时间戳,用于计算增量 | Unix 毫秒 |
关键认知:令牌生成速率 r 控制平均流量,桶容量 b 控制突发流量。两者独立配置,共同决定系统的限流行为。
2.2 令牌生成速率与桶容量
令牌桶的核心思想是:系统以恒定速率 r 向桶中注入令牌,桶最多容纳 b 个令牌。当请求到达时,必须从桶中消耗至少 1 个令牌才能被放行;如果桶为空,请求被拒绝或排队。
- 令牌生成速率(r):决定了系统的稳态吞吐量。例如 r=100,表示长期平均每秒最多处理 100 个请求。
- 桶容量(b):决定了系统允许的瞬时突发大小。例如 b=200,意味着在桶满的情况下,可以一次性处理 200 个请求,而不受 r 的限制。
设计原则:通常设置 b > r,使系统能应对短时流量尖峰。例如 r=100、b=200,允许 2 秒的突发流量。
2.3 突发流量处理
令牌桶相比漏桶算法的最大优势在于:允许突发。当桶内积累了大量令牌(最多 b 个),系统可以在短时间内以远高于 r 的速率处理请求,直到令牌耗尽。
突发处理流程:
- 空闲期:请求较少,令牌持续积累,桶逐渐变满。
- 突发到达:大量请求瞬间涌入,桶内令牌充足,所有请求立即被放行。
- 令牌耗尽:桶变空后,后续请求必须等待新令牌生成(受 r 限制),速率回落到 r。
注意:突发能力并非无限。最大突发请求数 = min(突发请求量, b)。如果突发请求量超过 b,超出的部分会被直接拒绝。
2.4 算法数学模型
令牌桶的数学模型基于时间差计算,避免使用定时器,适合在 Redis 中高效实现。
令牌补充公式:
// 当前时间 now,上次填充时间 t_last
// 时间间隔 delta = now - t_last(单位:秒)
// 应补充令牌数 = delta * r
// 新令牌数 = min(当前令牌数 + delta * r, b)
c_new = min(c + (now - t_last) * r, b)
请求消耗公式:
// 请求需要 n 个令牌(通常 n=1)
if c_new >= n:
放行请求
c_new = c_new - n
else:
拒绝请求
c_new 不变
完整单次请求算法(伪代码):
function allow_request(key, r, b, n=1):
now = current_time_ms()
c, t_last = redis.hget(key, "tokens", "last_time")
// 1. 补充令牌
delta = (now - t_last) / 1000.0
c = min(c + delta * r, b)
// 2. 判断是否放行
if c >= n:
c = c - n
redis.hset(key, "tokens", c, "last_time", now)
return true
else:
redis.hset(key, "tokens", c, "last_time", now)
return false
数学本质:令牌桶是一个带容量限制的积分器。生成速率 r 是积分系数,桶容量 b 是积分上限,请求消耗是负反馈。该模型保证了长期平均速率 ≤ r,同时允许短时速率 > r。
2.5 令牌桶状态变化图
下图展示了令牌桶在空闲、突发、限流三个阶段的状态变化:
2.6 令牌桶 vs 漏桶对比
| 对比维度 | 令牌桶 | 漏桶 |
|---|---|---|
| 突发处理 | 支持,桶容量决定突发上限 | 不支持,强制平滑输出 |
| 长期平均速率 | 严格 ≤ r | 严格 = r |
| 实现复杂度 | 中等,需维护时间戳 | 较低,队列模型 |
| 适用场景 | 允许短时突发的业务(API 网关、秒杀) | 要求绝对平滑的场景(视频流、数据同步) |
| Redis 实现 | 推荐,Lua 脚本高效 | 不推荐,队列操作复杂 |
选型建议:在微服务熔断与限流场景中,令牌桶是更优选择。它既能保护下游不被持续过载,又能容忍业务侧的合理突发。