12_Redis Lua 脚本踩坑:为什么 tonumber(ARGV[x]) 会得到 nil?

发布时间:2026/7/9 4:03:34
12_Redis Lua 脚本踩坑:为什么 tonumber(ARGV[x]) 会得到 nil? 最近在使用 Redis Lua 脚本做登录失败次数限制时遇到了一个比较容易忽略的问题。Lua 脚本里明明写了local maxCount tonumber(ARGV[1])但是运行时报错attempt to compare nil with number对应代码类似这样local currentCount tonumber(redis.call(GET, KEYS[1]) or 0) ​ if currentCount tonumber(ARGV[1]) then return -1 end乍一看会觉得奇怪ARGV[1]明明传的是登录失败次数上限比如5为什么tonumber(ARGV[1])会得到nil问题原因问题的关键不在 Lua 脚本本身而在 Java 侧执行 Lua 脚本时使用的 RedisTemplate 序列化方式。项目里常见的 RedisTemplate 配置是RedisTemplateString, Object并且 valueSerializer 可能配置为GenericJackson2JsonRedisSerializer这个配置用于存储对象、JSON 数据时很方便但在执行 Lua 脚本时需要注意传给 Lua 的 ARGV 参数也可能会经过 RedisTemplate 的序列化。比如 Java 里传的是String.valueOf(5)你以为 Lua 收到的是5但如果经过 JSON 序列化Lua 实际收到的可能是5这两个在 Lua 里是不一样的tonumber(5) -- 结果是 5 tonumber(\5\) -- 结果是 nil所以最终就会出现currentCount nil然后报错attempt to compare nil with number简单来说不是参数没传也不是 Lua 写错了而是参数被 RedisTemplate 序列化成了 Lua 不好直接识别的格式。方案一使用 StringRedisTemplate参数传 String.valueOf(...)这是比较推荐的方式。适合场景验证码发送限制登录失败次数限制限流计数器Lua 脚本参数都是字符串或数字。Java 侧使用StringRedisTemplateLong result stringRedisTemplate.execute( checkAndIncrementLoginFailScript, Collections.singletonList(failCountKey), String.valueOf(LOGIN_FAIL_MAX_COUNT), String.valueOf(LOGIN_LOCK_MINUTES * 60) );Lua 侧统一使用tonumber()-- 原子性地检查登录失败次数并累加 -- 如果失败次数已达上限返回 -1 -- 如果未达上限累加失败次数并返回累加后的值 -- -- KEYS[1]: 登录失败计数的 Redis Key如 login_fail_count:13800138001 -- ARGV[1]: 登录失败次数上限如 5 -- ARGV[2]: 锁定时间单位秒如 1800即 30 分钟 local currentCount tonumber(redis.call(GET, KEYS[1]) or 0) -- 如果失败次数已达上限直接返回 -1不累加 if currentCount tonumber(ARGV[1]) then return -1 end -- 累加失败次数 local newCount redis.call(INCR, KEYS[1]) -- 首次失败时设置过期时间锁定窗口 if newCount 1 then redis.call(EXPIRE, KEYS[1], ARGV[2]) end return newCount这种方式的好处是职责清晰RedisTemplateString, Object // 用来存对象、JSON StringRedisTemplate // 用来处理字符串、数字、Lua、计数器对于 Lua 脚本来说ARGV本身就是字符串参数所以 Java 端用String.valueOf(LOGIN_FAIL_MAX_COUNT)Lua 端再用tonumber(ARGV[1])这个约定比较清楚也比较稳定。方案二使用 RedisTemplate参数直接传数字也可以继续使用原来的RedisTemplateString, Object并且参数直接传数字Long result redisTemplate.execute( checkAndIncrementLoginFailScript, Collections.singletonList(failCountKey), LOGIN_FAIL_MAX_COUNT, LOGIN_LOCK_MINUTES * 60 );Lua 侧仍然要使用tonumber()-- 原子性地检查登录失败次数并累加 -- 如果失败次数已达上限返回 -1 -- 如果未达上限累加失败次数并返回累加后的值 -- -- KEYS[1]: 登录失败计数的 Redis Key如 login_fail_count:13800138001 -- ARGV[1]: 登录失败次数上限如 5 -- ARGV[2]: 锁定时间单位秒如 1800即 30 分钟 local currentCount tonumber(redis.call(GET, KEYS[1]) or 0) -- 如果失败次数已达上限直接返回 -1不累加 if currentCount tonumber(ARGV[1]) then return -1 end -- 累加失败次数 local newCount redis.call(INCR, KEYS[1]) -- 首次失败时设置过期时间锁定窗口 if newCount 1 then redis.call(EXPIRE, KEYS[1], ARGV[2]) end return newCount为什么这种方式也可以因为如果传的是数字例如5经过 JSON 序列化后通常还是5Lua 中tonumber(5)可以正常转成数字。但是如果传的是字符串String.valueOf(5)在某些 JSON 序列化配置下可能会变成5这时tonumber(\5\)就会得到nil。所以如果继续使用RedisTemplateString, Object需要注意不要把数字参数先转成 String 再交给 JSON 序列化器处理否则可能被序列化成带引号的 JSON 字符串。两种方案怎么选更推荐第一种StringRedisTemplate String.valueOf(...) Lua tonumber(...)因为它的类型约定最清楚Java 端明确传字符串形式的数字Redis 使用字符串序列化Lua 端统一用tonumber()转数字。第二种RedisTemplate 直接传数字 Lua tonumber(...)也可以用但前提是你清楚当前 RedisTemplate 的序列化行为。如果当前 RedisTemplate 主要是用来存对象、JSON那么它的 valueSerializer 可能会影响 Lua 参数的最终格式这类问题后续还可能再次出现。总结这次问题可以概括为RedisTemplate 使用 JSON 序列化时Lua 脚本的 ARGV 参数可能被序列化成非预期格式导致 tonumber(ARGV[x]) 得到 nil。最终建议如果是 Lua、限流、计数器、验证码、登录失败次数这类场景优先使用StringRedisTemplate使用StringRedisTemplate时Java 端参数可以传String.valueOf(xxx)Lua 脚本里参与数字计算和比较的参数统一使用tonumber(ARGV[x])如果继续使用RedisTemplateString, Object可以直接传数字参数不要先转成字符串再让 JSON 序列化器处理。一句话总结Lua 脚本问题不一定出在 Lua很多时候是 Java 侧 RedisTemplate 的序列化方式影响了 ARGV 的实际值。