eval返回nil或0因脚本末尾缺显式return;INCR+EXPIRE非原子致key永久存在;时间窗口需用TIME计算并设过期;Python调用须正确传keys/ARGV参数。

用 eval 执行限流 Lua 脚本时,为什么返回 nil 或 0?
Redis 的 eval 返回值完全取决于 Lua 脚本最后一行的 return。防刷脚本常靠返回 1/0 判断是否放行,但容易漏掉显式 return,或误把 redis.call() 的结果当最终返回值。
- 必须在脚本末尾写
return 1或return 0,不能只靠中间逻辑“影响”结果 - 如果用了
if分支,两个分支都得有return,否则未命中分支时返回nil(Python 客户端可能转成None,导致逻辑错判) - 避免在脚本里用
print或无返回值函数收尾,它们不产生eval输出
示例错误写法:if count —— 这里没 <code>return,结果恒为 nil。
INCR + EXPIRE 组合为什么在高并发下会失效?
单独用 INCR 加 EXPIRE 两步操作不是原子的,中间可能被其他客户端打断:key 被 INCR 了,但 EXPIRE 没执行成功(比如网络中断、客户端崩溃),导致 key 永久存在,后续所有请求都被误拦。
- 必须用
eval把计数和过期设置包进一个 Lua 脚本,靠 Redis 单线程保证原子性 - 更稳妥的做法是用
INCR后立即检查返回值,若为 1(即首次创建),再调用EXPIRE;但仍有窗口期,不推荐用于生产级防刷 - 直接用
SET key value EX seconds NX初始化 key 不适用限流,因为它不支持自增
正确思路:用 redis.call("incr", key) 后立刻 redis.call("expire", key, ttl),哪怕 key 已存在,expire 也会刷新过期时间。
Lua 脚本里怎么安全处理时间窗口(比如“每分钟最多 5 次”)?
不能只依赖固定 key,否则无法滚动窗口。常见做法是把时间戳嵌入 key 名,比如 "rate:uid:123:202405201430"(精确到分钟),但要注意:key 数量会随请求增长,需配合过期策略清理。
- 推荐用
TIME命令获取 Redis 服务端时间(避免客户端时间不同步),再计算窗口标识,例如:local now = redis.call("TIME")[1]; local window = math.floor(now / 60) * 60 - key 命名建议为
"rate:" .. user_id .. ":" .. window,然后对这个 keyINCR - 务必给每个窗口 key 设置过期时间(如
EXPIRE key 65),多留 5 秒容错,防止旧窗口 key 积压 - 不要用
KEYS扫描清理旧 key —— O(n) 复杂度,会阻塞 Redis;靠EXPIRE自动淘汰更安全
注意:窗口精度越高(如秒级),key 粒度越细,内存占用越大,需权衡业务容忍度。
Python 客户端调用 eval 防刷时,参数传错会怎样?
Redis Python 客户端(如 redis-py)要求 eval(script, numkeys, *keys, *args),其中 numkeys 是 keys 参数个数,不是总参数数。传错会导致 ERR Error running script 或 key 获取异常。
- 脚本里用
KEYS[1]就表示第一个 key,那调用时numkeys必须是 1,且第一个参数是 key 名,后面才是ARGV - 常见错误:把用户 ID 当作 key 传进去,但脚本里却用
ARGV[1]取,结果取到空或错值 - limit 和窗口时长这类配置项,统一走
ARGV,别硬编码在 Lua 里,方便复用和测试 - 调试时可先用
redis-cli --eval手动跑脚本,确认逻辑再集成到代码
例如:脚本期望 KEYS[1] = "rate:u123",ARGV[1] = "5"(limit),ARGV[2] = "60"(ttl),那 Python 调用就是 r.eval(script, 1, "rate:u123", 5, 60)。
真正难的不是写对那一段 Lua,而是想清楚窗口怎么切、key 生命周期怎么管、失败时怎么降级——这些地方没日志、没监控,出问题只会静默限流或完全不生效。


















