直接用incr+expire会出错,因二者非原子操作:若incr成功但expire失败(如连接中断),key永久存在致限流失效;高并发下多客户端同时设expire还会覆盖ttl,且多维key频繁创建加剧内存碎片。

为什么直接用 INCR + EXPIRE 做多维限流会出错
因为 Redis 的原子性只保证单命令,INCR 和 EXPIRE 是两个独立操作,中间可能被其他客户端插入,导致过期时间没设上、key 永久残留;更糟的是,在分布式多维场景下(比如按 user_id:api_path:minute 组合限流),频繁建 key + 设过期,会产生大量短生命周期 key,引发内存碎片和淘汰压力。
EVAL 中用 redis.call("INCR") 和 redis.call("PEXPIRE") 必须配对使用
Lua 脚本在 Redis 中是原子执行的,但得自己确保逻辑完整。常见错误是只 INCR 不设过期,或用 EXPIRE(秒级)而实际需要毫秒精度(比如 60 秒窗口要精确到 ms 级滑动)。正确做法是:
- 用
redis.call("INCR", key)初始化并累加计数 - 紧接着用
redis.call("PEXPIRE", key, window_ms)设置毫秒过期(如 60000 表示 60 秒) - 检查返回值:若
INCR返回 1,说明是新 key,必须设过期;若大于 1,说明已存在,PEXPIRE会刷新过期时间(Redis 7.0+ 支持,旧版需先TTL判断)
示例片段(限流窗口 60 秒,最大 100 次):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if current == 1 then
redis.call("PEXPIRE", key, window_ms)
end
if current > limit then
return 0
end
return 1
多维度 key 拼接必须防注入且保持可读性
维度太多时(如 user_id:app_id:ip:api_path:2024052014),手动拼接容易漏转义、长度失控、调试困难。关键点:
- 所有变量必须用
string.gsub(val, "[^%w:_-]", "")过滤非法字符,防止 Lua 注入或 Redis key 乱码 - 时间维度推荐用对齐窗口(如分钟级用
os.time() // 60),别用strftime类函数——Lua 在 Redis 中不支持os.date - 维度顺序要有业务意义,把变化最频繁的放最后(如
api_path:user_id:window_ts),方便用KEYS批量扫描时利用前缀
高并发下 Lua 脚本性能瓶颈不在逻辑,而在 key 数量和网络往返
Redis 执行 Lua 是单线程阻塞的,但脚本本身很快;真正拖慢的是:传入太多 KEYS(超过 10 个就明显延迟)、或每个请求都生成新 key 导致冷 key 频繁创建。优化方向:
- 把多维限流聚合到更粗粒度(如先按
user_id:hour检查总调用量,超了再进细粒度校验) - 用
EVALSHA替代EVAL,避免每次传输脚本体(首次SCRIPT LOAD后缓存 SHA1) - 客户端做简单预判:比如本地记录上次请求时间,间隔远小于窗口时直接拒绝,减少 Redis 调用
复杂点在于维度组合爆炸时的 key 管理——不是脚本写不对,而是 key 设计不合理导致内存涨得比限流还快。










