Redis Lua脚本可原子性完成计数、过期设置与窗口滑动,避免INCR+EXPIRE的竞态风险;需用tonumber()转换参数,KEYS[1]须唯一,ZADD用当前时间戳作member,EXPIRE加10–30秒缓冲。

Redis Lua 脚本能原子性完成「计数 + 过期设置 + 窗口滑动」三件事,是实现时间窗口限流最可靠的方式;单纯用 INCR + EXPIRE 有竞态风险,不推荐。
为什么必须用 Lua 而不是客户端分步操作
客户端先 INCR 再 EXPIRE 时,若进程在中间崩溃或网络中断,会导致 key 永久存在(无过期),后续所有请求都被误限流。Lua 脚本在 Redis 单线程中执行,天然原子——要么全成功,要么全失败。
常见错误现象:(error) ERR Error running script (call to f_...): @user_script:1: user_script:1: attempt to compare number with nil,通常因传入的 ARGV[1](窗口秒数)未转为数字,Lua 中需显式调用 tonumber()。
- 使用场景:API 接口每分钟最多 100 次调用,窗口需严格滚动(非固定整点)
-
KEYS[1]必须是带业务标识的唯一 key,例如"rate:ip:192.168.1.100"或"rate:uid:12345" - 不要把整个限流逻辑拆成多个 Lua 脚本,避免跨脚本状态不一致
标准滑动窗口 Lua 脚本写法(含注释)
以下脚本实现「N 秒内最多 M 次」的滑动窗口计数,自动清理过期记录:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local key = KEYS[1]
local max_count = tonumber(ARGV[1])
local window_seconds = tonumber(ARGV[2])
<p>-- 获取当前时间戳(秒级)
local now = tonumber(redis.call('TIME')[1])</p><p>-- 计算窗口起始时间戳
local window_start = now - window_seconds</p><p>-- 删除早于窗口起始时间的所有记录
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)</p><p>-- 插入当前时间戳(作为 score),并去重(member 可设为 request_id 或空字符串)
redis.call('ZADD', key, now, now)</p><p>-- 设置过期时间,防止 key 永久残留(过期时间略大于窗口,如 window+10s)
redis.call('EXPIRE', key, window_seconds + 10)</p><p>-- 获取当前窗口内元素数量
local count = tonumber(redis.call('ZCARD', key))</p><p>if count > max_count then
return 0 -- 超限
else
return 1 -- 通过
end注意:ZADD 的 member 建议用 now(而非随机字符串),避免同一秒内多次调用被合并成一个 member;EXPIRE 时间要留余量,否则可能刚插入就被删掉。
调用时的参数与兼容性要点
从客户端调用该脚本时,KEYS 和 ARGV 顺序不能错,且需确保 Redis 版本 ≥ 2.6(支持 Lua):
- Python 示例:
redis.eval(script, 1, "rate:ip:10.0.0.1", 100, 60)—— 表示“该 IP 60 秒内最多 100 次” - Java Jedis:
jedis.eval(script, 1, "rate:ip:10.0.0.1", "100", "60")—— 注意ARGV是字符串数组,Lua 内需tonumber() - Redis Cluster 下,
KEYS[1]必须落在同一 slot,否则报CROSSSLOT错误;可通过{rate:ip:10.0.0.1}加花括号强制哈希到同 slot - 性能影响:ZSET 操作复杂度为 O(log N),当单窗口内请求量极大(如万级/秒),ZSET 的内存和 CPU 开销会上升,此时应考虑降级为固定窗口(
INCR+EXPIRE)或改用 RedisTimeSeries
真正容易被忽略的是 EXPIRE 时间与 window_seconds 的差值——设得太小会导致 key 提前消失,设得太大又浪费内存;实际部署时建议统一加 10–30 秒缓冲,并配合监控看 INFO memory 中的 used_memory_peak 波动。

















