根本原因是Redis单线程模型下浮点截断、反复redis.call()、大循环及全局变量残留;须从脚本结构、参数精度、命令调用三方面修正:避免浮点除法导致delta=0,用local缓存读值,批量命令替代循环,强制SCRIPT LOAD+EVALSHA。

Redis Lua限流脚本卡顿、超时、结果不准,根本原因不是逻辑写错,而是踩中了单线程执行模型下的几个硬伤:浮点截断、反复调用 redis.call()、大循环清理、全局变量残留。必须从脚本结构、参数精度、命令调用三方面同步修正。
避免浮点数除法导致的 delta=0 问题
Lua 在 Redis 中对浮点数做算术运算后,若参与比较或赋值,小数部分会被隐式截断——这不是 bug,是 Redis 内置 Lua 解释器的确定性行为。限流脚本里常见错误是把毫秒级时间戳直接除以 1000 后参与 math.max 或 math.min,结果 delta 变成 0。
- 错误写法:
local delta = math.max(0, (now - last_refreshed) / 1000)→ 即使差值是 999ms,除完变 0.999,再被截断为 0 - 正确做法:先算毫秒级差值,再乘以 rate,最后统一除以 1000 并用
math.floor向下取整local delta_ms = now - last_refreshedlocal filled_tokens = math.min(capacity, last_tokens + math.floor(delta_ms * rate / 1000)) - 更稳妥:客户端传入已换算好的“每毫秒生成令牌数”,彻底规避除法,如
ARGV[1] = "0.001"(对应 1 token/s),脚本内直接delta_ms * rate
减少 redis.call() 调用次数,用 local 缓存中间值
每次 redis.call() 都触发完整命令生命周期:解析、ACL 校验、键路由(集群下)、执行、返回。哪怕读同一个 key 三次,开销也接近三次独立 GET。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 禁止重复读:
redis.call('GET', KEYS[1])出现两次以上,必须提取为local val = redis.call('GET', KEYS[1]) - 批量替代循环:不要用 for 循环调
redis.call('HGET', KEYS[1], ARGV[i]),改用redis.call('HMGET', KEYS[1], unpack(ARGV)) - 写操作优先原子原生命令:用
INCRBY替代GET+SET,用ZADD一次写多个 score-member 对,而非多次调用
滑动窗口脚本必须原子完成 ZREMRANGEBYSCORE + ZCARD + ZADD
拆成三步 Redis 命令调用,高并发下必然出现竞态:两个请求同时查到 ZCARD == 99,都判断“未超限”,然后都 ZADD,最终变成 101 —— 这不是概率问题,是确定性失败。
- 脚本内必须一次性完成:清理旧数据 → 统计当前数 → 条件写入新数据
-
ZREMRANGEBYSCORE的范围要严格闭区间:0到now - window_ms,注意边界时间点不能漏也不能重叠 -
ZCARD比ZCOUNT更快且稳定,后者在大数据量时可能慢一个数量级 - 必须显式
redis.call('EXPIRE', KEYS[1], expire_sec),ZSet 不会自动过期;expire_sec建议设为math.ceil(window_ms / 1000) + 1,防刚清空就过期
强制使用 SCRIPT LOAD + EVALSHA,禁用裸 EVAL
高频限流场景(如网关每秒数千次)下,每次 EVAL 发送几百字节脚本文本,网络、序列化、SHA 计算开销远超脚本本身执行时间。默认 lua-time-limit 是 5 秒,但传输和解析耗时也会计入总时限。
- 上线前必须
SCRIPT LOAD预加载,拿到 SHA1 值(如"a1b2c3d4...") - 客户端缓存该 SHA1,后续全部走
EVALSHA a1b2c3d4 1 my:rate:key 10 1000 ... - 首次调用失败(返回
NOSCRIPT)时,才 fallback 到EVAL并重新SCRIPT LOAD,不可每次都 fallback - 动态拼接脚本(如把阈值写死进字符串)等于放弃缓存,必须改为通过
ARGV传参
真正难的不是写出能跑的脚本,而是让脚本在每秒上千次调用、毫秒级响应、不丢令牌、不出竞态的前提下持续稳定——这要求你放弃“脚本能用就行”的思路,每一行 redis.call()、每一个除法、每一次变量声明,都要清楚它在 Redis 单线程模型里的真实成本。


















