因为INCR不带过期逻辑,键永久存在导致时间窗口无法重置;正确做法是用Lua脚本原子执行INCR和EXPIRE,确保首次自增即设TTL,并校验阈值。

为什么不能直接用 INCR 单独做限流
因为 INCR 本身不带过期逻辑,键一旦创建就永久存在(除非手动删或被淘汰),导致限流窗口无法重置。比如你设每分钟最多 10 次请求,但用户第一次调用后 INCR 生成的 key 一直留着,后续所有请求都会累加到同一个 key 上,完全失去“按时间窗口”限制的意义。
常见错误写法:INCR rate:uid:123 → 返回 1、2、3… 永远涨,没窗口概念。
正确思路是:每次请求都尝试初始化一个带过期时间的计数器,再原子增。但 Redis 没有「如果不存在则 SET 并 EXPIRE,再 INCR」的原生命令——所以得靠客户端逻辑兜底或使用 Lua 脚本。
用 Lua 脚本保证 INCR + EXPIRE 原子性
Redis 执行 Lua 脚本是原子的,脚本内多个命令不会被其他客户端打断。这是最稳妥的组合方式。
脚本逻辑要点:
- 先用
INCR尝试自增;若 key 不存在,INCR会自动初始化为 1 - 再用
EXPIRE设置过期时间;注意:对已存在的 key 调用EXPIRE是幂等的,但返回值是 0 或 1,不影响逻辑 - 最后判断当前计数值是否超限,返回结果
示例 Lua 脚本(限流 10 次 / 60 秒):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
if current > tonumber(ARGV[2]) then
return 0
end
return current
调用方式(以 redis-cli 为例):eval "..." 1 rate:uid:123 60 10
SET 的 XX/NX + EX 参数不能替代 INCR+EXPIRE
有人想用 SET key 1 EX 60 NX 初始化,再用 INCR,但这中间存在竞态窗口:两次命令之间可能有其他客户端插入操作,导致计数错乱。
关键区别:
-
SET ... NX只能确保「首次设置成功」,但无法保证「首次设置 + 后续自增」整体原子 -
INCR对不存在的 key 自动设初值为 1,行为确定;而SET需要显式传值,容易漏掉初始化逻辑 - 如果你用
GET判断再INCR,更是典型非原子操作,绝对不可用于生产限流
实际部署时要注意的三个细节
限流不是写完脚本就完事,这几个点常被忽略:
- 脚本里用
tonumber(ARGV[1])强制转数字,避免传入字符串 "60" 导致EXPIRE失败(Redis 5.0+ 会报错) - key 命名要有业务区分度,比如
rate:api:/login:ip:192.168.1.1,避免不同维度共用 key 导致误限 - 客户端必须检查 Lua 脚本返回值:0 表示触发限流,非 0 表示放行(返回值是当前计数,可用于日志调试)
超时时间(TTL)和限流阈值不宜设得太小,否则 Redis 频繁创建/删除 key 会增加内存碎片和 CPU 开销;建议最小窗口不低于 1 秒,阈值不低于 5。

















