首页/数据/Redis 进阶速查/滑动窗口限流:ZSet 加 Lua
数据结构与选型42 条

滑动窗口限流:ZSet 加 Lua

把请求时间戳作为 ZSet 分数:Lua 内先删除窗口外记录、再统计数量、未超限才写入并返回;整套动作原子完成,避免并发下的计数偏差。

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

滑动窗口限流是什么

固定窗口计数会在窗口边界处放过接近两倍的流量,滑动窗口则用「最近一段时间内的请求次数」来判定:把每次请求的时间戳作为 ZSet 分数存进去,判定时先删掉窗口外的旧记录,再数剩下的条数,未超限才把本次请求写入。整段逻辑必须放进 Lua 脚本,否则删除、计数、写入三步之间会被其他请求插入,得到错误的判定结果。

基本信息

数据结构ZSet,成员为带随机后缀的请求标识,分数为毫秒时间戳
窗口参数如 60 秒内允许 100 次,由调用方通过 ARGV 传入
原子范围ZREMRANGEBYSCORE 清理过期记录、ZCARD 计数、ZADD 写入、PEXPIRE 续期
返回约定通常返回 1 表示放行、0 表示拒绝,也可返回剩余额度
清理方式键随窗口过期自动消失,避免长期驻留

典型使用场景

  • 接口级限流:按用户 ID 或 IP 限制单位时间内的调用次数
  • 登录与验证码保护:限制同一手机号或 IP 在短时间内的尝试次数
  • 防止刷单与恶意爬取:按账号维度限制下单或抓取频率
  • 下游保护:对调用第三方接口的请求做配额控制,避免超出对方阈值
  • 精细流量整形:对高价值接口用滑动窗口,对普通接口用更省内存的固定窗口

Lua 脚本示意

Lua 脚本示意
local now = tonumber(ARGV[1])       -- 当前毫秒时间戳
local window = tonumber(ARGV[2])    -- 窗口长度,毫秒
local limit = tonumber(ARGV[3])     -- 窗口内允许次数
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)   -- 清理窗口外记录
local count = redis.call('ZCARD', KEYS[1])
if count < limit then
  redis.call('ZADD', KEYS[1], now, now .. '-' .. math.random(1000000))  -- 成员必须唯一
  redis.call('PEXPIRE', KEYS[1], window)
  return 1
end
return 0

调用与观察

调用与观察
redis-cli --eval limit.lua rate:user:1001 , 1730021000000 60000 100
redis-cli ZCARD rate:user:1001          # 当前窗口内已记录的请求数
redis-cli ZRANGE rate:user:1001 0 -1 WITHSCORES | head -n 5
redis-cli TTL rate:user:1001            # 空闲后键会自动消失

限流方案对照

方案精度内存开销
固定窗口计数窗口边界可能放过约两倍流量每对象一个计数键,最省
滑动窗口 ZSet严格按最近窗口内次数判定每次请求一条记录,开销与频率正相关
令牌桶允许短时突发,平滑放行需要额外维护令牌数与时间戳
漏桶输出速率恒定需要队列承载等待请求
成员必须带随机后缀:同一毫秒内到达的多个请求时间戳相同,ZSet 会把它们当成同一个成员覆盖掉,实际只记了一条。加上随机数或请求序号后成员唯一,计数才准确。
限流阈值要与业务匹配:过严会误伤正常用户,过松则起不到保护作用。上线前用真实流量分布估算合理阈值,并对被限流请求做监控与告警,否则只能等用户投诉才发现阈值设置错误。

易错点

  • 把清理、计数、写入拆成多条命令执行,并发下判定结果失真,超限请求照样被放行,整套逻辑必须在单个 Lua 脚本内完成
  • ZSet 成员直接用时间戳导致同毫秒请求互相覆盖,计数偏小,加随机后缀是最简单的修法
  • 忘记设置键的过期时间,空闲用户的限流键永久残留,内存随对象数量持续增长
  • 高频接口逐请求记录,内存与 ZSet 操作开销被放大,频率很高的场景应改用固定窗口或令牌桶方案

数据来源与参考

  • Redis 官方文档:EVAL 与 ZSet 组合的限流模式说明
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌