滑动窗口限流是什么
固定窗口计数会在窗口边界处放过接近两倍的流量,滑动窗口则用「最近一段时间内的请求次数」来判定:把每次请求的时间戳作为 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 会把它们当成同一个成员覆盖掉,实际只记了一条。加上随机数或请求序号后成员唯一,计数才准确。
限流阈值要与业务匹配:过严会误伤正常用户,过松则起不到保护作用。上线前用真实流量分布估算合理阈值,并对被限流请求做监控与告警,否则只能等用户投诉才发现阈值设置错误。