首页/数据/Redis 进阶速查/令牌桶限流:Hash 加时间差补桶
数据结构与选型42 条

令牌桶限流:Hash 加时间差补桶

用 Hash 保存剩余令牌数与上次补充时间:每次请求按流逝时间补令牌、够则扣减放行;适合允许突发流量的场景。

Redis 进阶速查数据更新于 2026-10-13

令牌桶的基本模型

令牌桶是限流中最常用的模型之一:桶按固定速率补充令牌,容量有上限;每个请求消耗一个令牌,令牌不足就拒绝。它允许不超过桶容量的突发流量,比固定窗口计数更平滑,也不会出现窗口切换瞬间的双倍放行问题。在 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 命令,或由调用方传入统一时间),客户端各自取本地时间会因时钟漂移导致补桶量算错。桶容量与速率要通过压测校准,凭感觉设定的参数在促销或活动流量下会立刻失效。

易错点

  • 把「读令牌、判断、扣减」拆成多条命令执行,并发请求读到相同的令牌数,直接出现超发
  • 用客户端本地时间作时间源,多台机器时钟不一致时补桶量计算错误
  • 忘记给限流键设置过期时间,冷用户与历史维度的键长期占用内存
  • 参数放得太宽(burst 是 rate 的几十倍),突发容量足够绕过限制,限流形同虚设

数据来源与参考

  • Redis 官方文档:EVAL 脚本模式;令牌桶算法通用说明
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌