12_Redis Lua 脚本踩坑:为什么 tonumber(ARGV[x]) 会得到 nil?
2026/7/9 4:03:20 网站建设 项目流程

最近在使用 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 配置是:

RedisTemplate<String, 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 侧使用StringRedisTemplate

Long 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

这种方式的好处是职责清晰:

RedisTemplate<String, Object> // 用来存对象、JSON StringRedisTemplate // 用来处理字符串、数字、Lua、计数器

对于 Lua 脚本来说,ARGV本身就是字符串参数,所以 Java 端用:

String.valueOf(LOGIN_FAIL_MAX_COUNT)

Lua 端再用:

tonumber(ARGV[1])

这个约定比较清楚,也比较稳定。


方案二:使用 RedisTemplate,参数直接传数字

也可以继续使用原来的RedisTemplate<String, 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 序列化后,通常还是:

5

Lua 中:

tonumber("5")

可以正常转成数字。

但是如果传的是字符串:

String.valueOf(5)

在某些 JSON 序列化配置下,可能会变成:

"5"

这时:

tonumber("\"5\"")

就会得到nil

所以如果继续使用RedisTemplate<String, 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。

最终建议:

  1. 如果是 Lua、限流、计数器、验证码、登录失败次数这类场景,优先使用StringRedisTemplate

  2. 使用StringRedisTemplate时,Java 端参数可以传String.valueOf(xxx)

  3. Lua 脚本里参与数字计算和比较的参数,统一使用tonumber(ARGV[x])

  4. 如果继续使用RedisTemplate<String, Object>,可以直接传数字参数,不要先转成字符串再让 JSON 序列化器处理。

一句话总结:

Lua 脚本问题不一定出在 Lua,很多时候是 Java 侧 RedisTemplate 的序列化方式影响了 ARGV 的实际值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询