更平滑的限流实现
固定窗口限流在窗口切换时会出现瞬时双倍放行,滑动窗口与令牌桶能缓解这一问题:滑动窗口用有序集合保存请求时间戳,每次请求先清理过期成员再判断数量;令牌桶用 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 脚本里。
参数怎么定
- 限流阈值可先按业务峰值的三分之一到二分之一起步,再按实测调整
- 窗口越小限流越平滑,但键数量与内存开销同步上升