首页/数据/数据库速查/滑动窗口与令牌桶限流(Lua 原子化)
Redis 运维与场景124 条

滑动窗口与令牌桶限流(Lua 原子化)

更平滑的限流:滑动窗口用 ZSet 存请求时间戳,令牌桶用 Hash 存令牌数与上次填充时间;判断与写入必须放进 Lua 一次原子完成。

数据库速查数据更新于 2026-10-13

更平滑的限流实现

固定窗口限流在窗口切换时会出现瞬时双倍放行,滑动窗口与令牌桶能缓解这一问题:滑动窗口用有序集合保存请求时间戳,每次请求先清理过期成员再判断数量;令牌桶用 Hash 保存当前令牌数与上次填充时间,按速率补充。无论哪种,判断与写入都必须放进 Lua 脚本一次完成,否则并发下必然超限。

基本信息

适用Redis 2.6 及以上,Lua 脚本通过 EVAL 原子执行
滑动窗口结构ZSet 成员为请求,score 为时间戳
令牌桶结构Hash 保存 tokens 与 ts 两个字段
关键点请求 ID 必须唯一,同毫秒的多请求不能互相覆盖

滑动窗口 Lua 脚本

滑动窗口 Lua 脚本
-- KEYS[1]=限流键; ARGV: now_ms, window_ms, limit, 唯一请求ID
local now = tonumber(ARGV[1])
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - tonumber(ARGV[2]))   -- 清理窗口外的旧请求
if redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[3]) then return 0 end  -- 达到上限直接拒绝
redis.call('ZADD', KEYS[1], now, ARGV[4])                              -- 记录本次请求
redis.call('PEXPIRE', KEYS[1], ARGV[2])                                -- 设置过期,避免空键残留
return 1
-- 调用:EVAL script 1 limit:uid:api <now_ms> <window_ms> <limit> <request_id>

常见限流方案对比

方案结构特点
固定窗口String 计数 + EXPIRE实现最简,窗口切换时可能瞬时双倍放行
滑动窗口ZSet 时间戳边界平滑,内存随请求量增长
令牌桶Hash 存令牌数与时间戳允许一定突发,速率可控
漏桶队列或计数输出速率恒定,突发会被排队
网关层限流网关内计数不占用 Redis 资源,适合全局粗粒度

典型使用场景

  • 接口级限流:按用户或 IP 限制单位时间请求数
  • 短信与支付等敏感操作的频率控制
  • 爬虫防护:按来源限制访问速率
  • 压测保护:测试账号设置更严格的额度
  • 多租户配额:按租户维度限制调用量
高 QPS 下 ZSet 滑动窗口每个请求一个成员,内存开销明显;大流量场景常改为固定窗口与令牌桶组合,或在网关侧做第一层限流。
用 GET 再 SET 的分步实现会在并发下超限:判断与写入必须放在同一条命令或同一个 Lua 脚本里。

参数怎么定

  • 限流阈值可先按业务峰值的三分之一到二分之一起步,再按实测调整
  • 窗口越小限流越平滑,但键数量与内存开销同步上升

易错点

  • GET 与 SET 分两步实现限流:并发场景直接超限,必须用 Lua 或单命令
  • 客户端时间回拨:窗口判断失真,应使用服务端时间或强制单调时钟
  • 限流键不设过期:空闲用户的历史键长期占内存,需要 PEXPIRE 兜底
  • 请求 ID 不唯一:同毫秒多请求在 ZSet 里互相覆盖,计数偏少导致放行超标

数据来源与参考

  • Redis 官方文档: EVAL
  • 限流模式实践(令牌桶/滑动窗口)

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌