位置:首页 > Lua > Redis Lua 脚本踩坑复盘:为什么 `GET` 后做加法,会报 boolean 错误

Redis Lua 脚本踩坑复盘:为什么 `GET` 后做加法,会报 boolean 错误

时间:2026-08-24  |  作者:清风无痕  |  阅读:0

本文已参加「新人创作礼」活动,一起开启掘金创作之路。

Round 1

一个记录文章阅读时长的接口,前端每秒钟轮询一次,后端根据阅读记录ID更新阅读记录时长,阅读记录在用户打开文章时创建。

一开始,后端接收到参数后直接存入MySQL。当阅读文章的用户较多时,就会给数据库造成压力。于是改为先将阅读时间戳和阅读时长存入Redis,使用一个定时任务来周期性扫描,如果发现用户一定时间没有读文章(根据阅读时间戳和当前时间的间隔来判断),视为已经结束阅读,将阅读时长批量存入MySQL。

由于需要同时操作两个key(阅读时间戳和阅读时长),使用Lua脚本来确保事务。此时的脚本非常简单:

--- 阅读时间戳key
local timeKey = KEYS[1]
--- 阅读时长key
local durationKey = KEYS[2]
--- 当前时间戳
local current = ARGV[1]
--- 过期时间
local expire = ARGV[2]

--- 阅读时间戳更新为当前时间戳
redis.call("set", timeKey, current)
--- 阅读时长增加1秒
redis.call("incr", durationKey)
--- 设置过期时间
redis.call("expire", timeKey, expire)
redis.call("expire", durationKey, expire)

return true

Round 2

上面的脚本上线后运行良好。但是后来出现了新的场景,对于公众号文章,前端可以拿到总的阅读时长。同样的接口,但是现在增加了阅读时长参数,上面的脚本显然不适用了,因为阅读时长的增长是固定的1秒。于是我把上面的脚本改了一下:

--- 阅读时间戳key
local timeKey = KEYS[1]
--- 阅读时长key
local durationKey = KEYS[2]
--- 当前时间戳
local current = ARGV[1]
--- 过期时间
local expire = ARGV[2]
--- 阅读时长秒数
local seconds = ARGV[3]

--- 阅读时间戳更新为当前时间戳
redis.call("set", timeKey, current)
--- 阅读时长增加seconds秒
redis.call("set", durationKey, redis.call("get", durationKey) + seconds)
--- 设置过期时间
redis.call("expire", timeKey, expire)
redis.call("expire", durationKey, expire)

return true

然后测试的时候就报了个错:

org.springframework.dao.InvalidDataAccessApiUsageException: ERR Error running script (call to f_f6ea343b2df0bb427a068c67e15fa9a65030e89e): @user_script:13: user_script:13: attempt to perform arithmetic on a boolean value

关键信息在最后,说我试图对一个布尔值进行算数运算。布尔值?

来瞧瞧新脚本,涉及运算的仅有redis.call("get", durationKey) + seconds。seconds肯定不是布尔值,那只能是redis.call("get", durationKey)的问题了。可我觉得,就算durationKey不存在,这里也该返回nil呀,怎么会是布尔值呢?难道是Spring把Lua的nil当成布尔值的false了?

为了验证我的猜想,我把“阅读时长增加seconds秒”改成了这样:

local oldDuration = redis.call("get", durationKey)
if oldDuration == nil then
    redis.call("set", durationKey, second)
else
    redis.call("set", durationKey, oldDuration + second)
end

如果没有旧阅读时长,直接set新阅读时长;如果有旧阅读时长,则set新阅读时长与旧阅读时长的和。简单明了,不可能出错。然而测试结果打破了我的自信。

不会吧不会吧,不会真的get到一个布尔值吧?

抱着姑且试试的想法,我给上面的程序加了个判断:

local oldDuration = redis.call("get", durationKey)
if oldDuration == nil or type(oldDuration) == 'boolean' then
    redis.call("set", durationKey, second)
else
    redis.call("set", durationKey, oldDuration + second)
end

程序好了。我TM……

尽管程序好了,但是技术人的执着驱使我必须弄懂为什么。最终我在官方文档上找到这样的一段话:

image.png

简单来说,我之前的猜测很接近事实,确实存在类型转换,不过并非Spring将Lua的nil视为false,而是Lua把Redis的nil当作了false。也就是说,责任在Lua这边。具体情况可查看官方文档: redis.io/docs/manual…

需要往下翻一翻,或者可以直接搜索Data type conversion。

get到这个知识点之后,我发现自己的脚本写的啰嗦了。于是我又改了一下,最后变成了这样:

--- 阅读时间戳key
local timeKey = KEYS[1]
--- 阅读时长key
local durationKey = KEYS[2]
--- 当前时间戳
local current = ARGV[1]
--- 过期时间
local expire = ARGV[2]
--- 阅读时长秒数
local seconds = ARGV[3]

--- 阅读时间戳更新为当前时间戳
redis.call("set", timeKey, current)
--- 阅读时长增加seconds秒
local oldDuration = redis.call("get", durationKey)
if not oldDuration then
    redis.call("set", durationKey, second)
else
    redis.call("set", durationKey, oldDuration + second)
end
--- 设置过期时间
redis.call("expire", timeKey, expire)
redis.call("expire", durationKey, expire)

return true

这里需要注意的一点是,Lua的取反符号是not,而不是Ja va的!,也不是~。

《夸克》非常好用的免费AI浏览器

下载APP查看

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多