直接用 INCR+EXPIRE 会出错,因为二者非原子操作,中间可能被中断或并发覆盖,导致漏限流;正确做法是用 Lua 脚本封装“查旧值、判超限、更新时间戳、写新值”四步,通过 EVAL/EVALSHA 保证原子性。

为什么直接用 INCR + EXPIRE 做滑动窗口会出错
因为 Redis 的原子性只保证单命令,INCR 和 EXPIRE 是两个独立操作,中间可能被中断或并发覆盖。比如窗口时间刚过期、新请求进来时,旧 key 还没删干净,新计数又从 0 开始,导致漏限流。滑动窗口本质要“查旧值、算是否超限、更新时间戳、写新值”,四步必须原子执行。
正确做法是把整个逻辑塞进 Lua 脚本里,靠 Redis 的 EVAL 保证原子性。脚本里用 redis.call() 操作数据,所有读写都在服务端一次完成。
- 别在客户端做 if-else 判断后再发命令——网络延迟+竞态会让结果不可信
- 别用
SETNX+EXPIRE模拟——同样非原子,且无法表达“过去 N 秒内总请求数”这个语义 - 注意 Lua 脚本里不能用
os.time(),要用redis.call('TIME')获取服务端时间,避免客户端和服务端时钟不同步
EVAL 脚本里怎么维护滑动窗口的时间槽
滑动窗口不是简单存一个总数,而是按时间切片(比如每秒一个槽),记录每个时间段的请求数,再对最近 W 个槽求和。Lua 脚本里常用 zset 或 hash 实现:前者用时间戳作 score 存请求次数,后者用秒级时间戳作 field 存计数。推荐用 zset,天然支持范围查询和自动剔除过期槽。
关键点是每次请求都要:ZREMRANGEBYSCORE 清理过期槽 → ZSCORE 查当前槽是否存在 → ZADD 写入或自增 → ZRANGEBYSCORE 算窗口内总和 → ZCARD 或求和判断是否超限。
- 窗口宽度(如 60 秒)和时间精度(如 1 秒/槽)要提前定死,不能在脚本里动态算,否则影响性能
- 用
zset时,score 必须是整数时间戳(单位秒),别用毫秒——RedisZRANGEBYSCORE对浮点 score 支持弱,且精度冗余 - 如果 QPS 很高(万级/秒),避免每次
ZRANGEBYSCORE扫全量;改用ZCOUNT统计数量,或预存一个总和 key 配合HINCRBY更新
实际部署时 Lua 脚本怎么传、怎么调、怎么防误用
别每次请求都用 EVAL 发一长串脚本——网络开销大,且 Redis 无法缓存编译结果。应该先用 SCRIPT LOAD 把脚本加载进服务端,拿到 SHA1 校验值,之后用 EVALSHA 调用,既快又安全。
调用时注意传参顺序:EVALSHA <sha> <numkeys> <key1> <key2> ... <arg1> <arg2> ...。其中 keys 必须是真实 Redis key(会被 cluster 路由识别),args 是纯参数(如窗口大小、限流阈值)。
- key 名建议带业务前缀,比如
rate:login:{uid},避免不同接口共用 key 导致误限流 - args 里不要传复杂结构,Lua 脚本里不解析 JSON;数字就传数字,布尔就传
1/0 - 本地开发调试用
EVAL没问题,上线后必须切到EVALSHA,否则集群环境下可能因 script cache 不一致报NOSCRIPT错误
常见错误现象和对应检查点
限流失效、误触发、脚本超时——八成出在边界条件没处理好。比如窗口刚初始化时没设初始值,或者时间戳计算跨了秒级边界,导致某一秒的计数被漏掉。
典型报错:(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. 表示脚本卡死(比如 while 循环没出口、或用了耗时命令如 KEYS);(error) ERR Error running script (call to f_...): @user_script:xx: user_script:xx: attempt to compare number with string 是类型混用,Lua 里 redis.call() 返回的都是字符串,得显式 tonumber()。
- 脚本里所有
redis.call()结果,只要参与数值运算,必须tonumber()转换,哪怕看起来是数字 - 用
redis.call('TIME')拿到的是{seconds, microseconds}表,取res[1]就够,别直接当数字用 - 测试时别只压测单 key,要模拟多用户(不同 key)并发,否则发现不了 key 级别的竞争问题
if 分支是否覆盖了 time == nil、count == false、zset 为空这些真实存在的空状态。这些地方漏了,线上就是静默失效。










