用 INCR+EXPIRE 组合可模拟滑动窗口限流:按时间分桶生成 key,INCR 累加并首次调用时 EXPIRE 设过期,须用 Lua 原子执行防竞态,键名含时间戳便于审计,不推荐 CL.THROTTLE 因其非真正滑动窗口且集群受限。

用 INCR + EXPIRE 组合实现带滑动窗口的计数
Redis 本身不支持原生滑动窗口限流,但可以用 INCR 和 EXPIRE 配合时间戳键名模拟。核心思路是:把请求时间按粒度(比如 60 秒)做 floor 分桶,每个桶对应一个 key,INCR 累加,再用 EXPIRE 设定过期时间,避免手动清理。
常见错误是直接对固定 key 做 INCR,结果变成全局计数,无法体现“最近 N 秒内”的动态性;或者漏掉 EXPIRE,导致 key 永久残留、内存泄漏。
- 键名建议格式:
rate:uid:12345:60s_1712340000(其中1712340000是Math.floor(Date.now() / 60000) * 60得到的分钟级时间戳) - 每次请求先计算当前分桶时间戳,拼出 key,再执行
INCR;如果返回值为 1(即新 key),立刻跟一个EXPIRE设 61 秒(比窗口长 1 秒,防临界丢失) - 注意客户端时间可能不准,生产环境应以 Redis 服务端时间为基准,可用
TIME命令或 Lua 脚本内用redis.call("TIME")获取
用 Lua 脚本保证原子性,避免竞态导致超限
单纯用两个命令(INCR + EXPIRE)在高并发下会出问题:比如两个请求同时发现 key 不存在,都执行了 INCR,又都设了 EXPIRE,结果计数多加一次,且过期时间被重复设置——这会让风控失效。
必须用 Lua 把判断、累加、设过期封装成原子操作。Redis 保证脚本内命令顺序执行,且不会被中断。
- 脚本里不要用
KEYS,改用传入的 key 参数,否则集群模式报错CROSSSLOT Keys in request don't hash to the same slot - 返回值建议设计为:-1 表示拒绝,0 表示通过,正整数表示当前窗口剩余次数(方便前端展示或日志追踪)
- 示例关键逻辑:
local current = redis.call("INCR", KEYS[1]) if current == 1 then redis.call("EXPIRE", KEYS[1], ARGV[1]) end if current > tonumber(ARGV[2]) then return -1 end return tonumber(ARGV[2]) - current
为什么不用 CL.THROTTLE(Redis 6.2+ 的原生命令)
CL.THROTTLE 看起来是现成解法,但它有几个硬伤:不支持自定义窗口起始时间(只能从第一次调用算起)、响应结构复杂(返回 5 元素数组)、集群模式下 key 必须落在同一 slot(需用 {} 手动哈希对齐)、且无法和业务 key(如用户 ID)自然组合做复合限流。
更关键的是,它内部仍基于固定 key 的计数器,没有真正实现滑动窗口语义——只是用令牌桶近似,对“严格限制最近 60 秒内最多 100 次”这类策略,误差可能达一个窗口周期。
- 如果你的场景是“每小时最多发 10 条短信”,
CL.THROTTLE可用;但要是“每秒最多 5 次登录尝试”,就得自己实现分桶 - 某些云 Redis 服务(如阿里云 Tair)禁用了
CL.THROTTLE,直接调用会返回ERR unknown command - 它不记录具体时间戳,没法做审计回溯,而自建分桶 key 名天然带时间信息
自适应的关键:根据历史水位动态调整阈值
所谓“自适应”,不是让 Redis 自己学,而是业务层定期读取各分桶 key 的 TTL 和 GET 值,统计单位时间内的真实请求分布,再用配置中心或 DB 更新不同用户/接口的限流阈值。
容易被忽略的一点是:不能只看峰值,要结合衰减因子。比如某用户过去 24 小时每分钟平均请求 20 次,但最近 5 分钟突增至 80 次,这时该降级而非放行——因为高频可能是攻击前兆。
- 推荐用 Redis 的
SCAN扫描rate:uid:*:60s_*类 pattern,配合TTL过滤有效 key,再聚合求均值/标准差 - 阈值更新不能实时推送到所有实例,建议加一层本地缓存(如 Caffeine),TTL 设为 30 秒,避免 Redis 成为瓶颈
- 别忘了给“白名单用户”留后门:可在 Lua 脚本开头查一个
whitelist:{uid}的 set,命中则跳过限流逻辑
事情说清了就结束。真正的难点不在 Redis 命令怎么写,而在怎么定义“异常流量”——时间粒度选不对,整个滑动窗口就偏了;用户分群没做好,误杀和漏放会同时发生。










