令牌桶的基本模型
令牌桶是限流中最常用的模型之一:桶按固定速率补充令牌,容量有上限;每个请求消耗一个令牌,令牌不足就拒绝。它允许不超过桶容量的突发流量,比固定窗口计数更平滑,也不会出现窗口切换瞬间的双倍放行问题。在 Redis 里实现的关键是「读改写必须原子」:补齐令牌、判断、扣减、写回要在一次不可中断的操作里完成,否则并发下会超发。
基本信息
| 数据结构 | Hash,字段 tokens 存剩余令牌数、ts 存上次补充时间 |
|---|---|
| 参数 | rate 每秒补充速率,burst 桶容量 |
| 补桶公式 | tokens = min(burst, tokens + 流逝秒数 × rate) |
| 判定规则 | 令牌不少于 1 时放行并扣减一个 |
| 原子性 | 必须用 Lua 脚本或事务保证 |
典型使用场景
- 对外 API 按调用方限流,容忍短时突发但约束长期平均速率
- 短信、推送这类有真实成本的外部调用做削峰保护
- 保护下游数据库:读请求按桶放行,避免缓存失效时雪崩
- 单用户维度限流,如登录尝试、验证码发送频率控制
- 与滑动窗口选型对比:需要允许突发时优先选令牌桶
Lua 脚本实现
Lua 脚本实现
local rate = tonumber(ARGV[1]) -- 每秒补充令牌数
local burst = tonumber(ARGV[2]) -- 桶容量上限
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local t = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(t[1]) or burst -- 首次访问时桶是满的
local ts = tonumber(t[2]) or now
local delta = math.max(0, now - ts) / 1000 -- 流逝秒数,防止时钟回退
local filled = math.min(burst, tokens + delta * rate)
local allowed = 0
if filled >= 1 then filled = filled - 1; allowed = 1 end
redis.call('HSET', KEYS[1], 'tokens', filled, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(burst / rate * 2000)) -- 冷键自动回收
return allowed参数与调优方向
| 参数 | 作用 | 调优方向 |
|---|---|---|
| rate | 每秒补充的令牌数 | 等于长期允许的平均速率 |
| burst | 桶容量上限 | 决定可容忍的瞬时突发量 |
| PEXPIRE | 键的过期时间 | 按桶注满所需时间的两倍估算 |
| 返回值 1 与 0 | 放行与拒绝 | 客户端据此决定是否重试 |
时间源建议使用服务端时间(TIME 命令,或由调用方传入统一时间),客户端各自取本地时间会因时钟漂移导致补桶量算错。桶容量与速率要通过压测校准,凭感觉设定的参数在促销或活动流量下会立刻失效。