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 配置是:

复制代码
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 的实际值。

相关推荐
奶油话梅糖1 小时前
锐捷常用命令速查表
java·网络·windows
code_std2 小时前
java WebSocket 使用
java·开发语言·websocket
落叶-IT2 小时前
Java Scanner 类精讲:控制台交互
java·开发语言
chh5632 小时前
C++--string
java·开发语言·网络·c++·学习
Listen·Rain2 小时前
向量化详解
java·人工智能·spring·机器学习·ai
想你依然心痛2 小时前
AUTOSAR Classic Platform架构解析:BSW、RTE与ASW——分层、配置、代码生成
java·架构·实时互动
梦想的旅途22 小时前
企业微信外部群智能自动回复:基于RPA接口的关键词匹配方案
java·开发语言·数据库·机器人·自动化·企业微信
娇气的红酒2 小时前
关于.NET VS JavaEE平台争论的沉思录
java·java-ee·.net
折哥的程序人生 · 物流技术专研2 小时前
第3篇:手写一个饮品制作模板(附代码)
java·设计模式·行为型模式·模版方法模式·钩子方法·代码复用·编程实战