编写 Redis Lua 脚本的注意事项:避开这 8 个坑

编写 Redis Lua 脚本的注意事项:避开这 8 个坑

1. 使用 KEYS 和 ARGV,不要硬编码 key 名

-- ❌ 错误:硬编码 key 名
redis.call('SET', 'mykey', 'value')

-- ✅ 正确:通过 KEYS 数组传递 key 名
redis.call('SET', KEYS[1], ARGV[1])

原因:硬编码 key 名会导致:
– 脚本无法在不同环境(开发/测试/生产)间复用
– Redis Cluster 模式下无法正确路由到对应节点
– 运维时无法判断脚本操作的 key

2. 控制脚本执行时间

-- ❌ 危险:无限循环
while true do
    -- 死循环!
end

-- ❌ 危险:大量数据遍历
local keys = redis.call('KEYS', '*')  -- 可能包含百万级 key
for i, key in ipairs(keys) do
    -- 处理每个 key...
end

最佳实践
– 脚本执行时间控制在 1ms 以内
– 默认 lua-time-limit 为 5 秒,建议不要接近这个值
– 不要在生产环境调用 KEYS 命令
– 大数据量操作使用 SCAN 替代 KEYS

3. 正确处理数据类型

-- ❌ 错误:假设返回值总是字符串
local val = redis.call('GET', KEYS[1])
local num = tonumber(val) + 1  -- 如果 key 不存在,val 为 false,tonumber 返回 nil

-- ✅ 正确:检查 nil
local val = redis.call('GET', KEYS[1])
if val then
    return tonumber(val) + 1
else
    return 0
end

4. 避免在脚本中生成随机值

-- ❌ 可能的问题:随机值导致脚本在不同节点上执行结果不同
local random = math.random()
redis.call('SET', KEYS[1], random)

-- ✅ 如果确实需要随机值,通过 ARGV 从客户端传入
redis.call('SET', KEYS[1], ARGV[1])

原因:在 Redis Cluster 中,如果主从切换,从节点执行脚本时可能会产生不同的随机值,导致数据不一致。

5. 谨慎使用全局变量

-- ❌ 错误:使用全局变量
count = 0  -- 全局变量
for i, key in ipairs(KEYS) do
    count = count + 1
end

-- ✅ 正确:使用 local
local count = 0
for i, key in ipairs(KEYS) do
    count = count + 1
end

原因:Lua 脚本在沙箱环境中执行,全局变量可能被其他脚本污染或导致意外行为。

6. 了解可用的 Redis 函数

可以使用redis.call()redis.pcall()redis.log()redis.setresp()

  • redis.call():执行命令,出错时抛出异常
  • redis.pcall():执行命令,出错时返回错误对象(不抛异常)
  • redis.log():打印日志(redis.LOG_WARNINGredis.LOG_NOTICEredis.LOG_VERBOSEredis.LOG_DEBUG
local ok, err = redis.pcall('SET', KEYS[1], ARGV[1])
if not ok then
    redis.log(redis.LOG_WARNING, 'SET failed: ' .. tostring(err))
    return {err = err}
end

不可用:部分 Lua 标准库函数如 dofileloadfilerequireioos 被禁止。

7. 注意返回值格式

-- 返回单个值
redis.call('GET', KEYS[1])  -- 返回字符串

-- 返回多个值
return {redis.call('GET', KEYS[1]), redis.call('GET', KEYS[2])}

-- 返回 status reply
return redis.status_reply('OK')

-- 返回 error reply
return redis.error_reply('Something went wrong')

8. 测试和监控

-- 开发调试:使用 redis.log 打印信息
redis.log(redis.LOG_NOTICE, "Starting balance check for " .. KEYS[1])
local balance = redis.call('GET', KEYS[1])
redis.log(redis.LOG_NOTICE, "Current balance: " .. tostring(balance))

性能检查清单

-- 高效脚本模板
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])

-- ✅ 所有变量都是 local
-- ✅ 没有遍历大量数据
-- ✅ 没有硬编码 key
-- ✅ 执行时间可控
-- ✅ 正确处理 nil 值

local current = redis.call('GET', key)
if current and tonumber(current) >= limit then
    return 0
end
redis.call('INCR', key)
redis.call('EXPIRE', key, ttl)
return 1

面试要点

  • 一定要用 local 声明变量
  • 脚本内不可调用 KEYS * 等慢命令
  • 执行时间控制在毫秒级
  • Redis Cluster 下脚本内 key 必须在 slot 相同的节点上
  • redis.pcall() 代替 redis.call() 处理可选错误
© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容