登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis Lua 脚本怎样通过 KEYS 和 ARGV 区分参数

来源:17golang原创

时间:2026-10-23 05:00:43 313浏览 收藏

我在排查一段 Redis Lua 脚本时,最先怀疑的是脚本逻辑,最后发现问题出在调用参数的分界:业务值被放进了 KEYS,真正的 key 却没有按 numkeys 传入。结果不是简单的“取不到值”,而是脚本在单机环境里看起来能跑,换到集群或换一组参数后就变得难以判断。

记住一个规则就能拆开这类问题:KEYS 只放 Redis key 的名字,ARGV 放阈值、状态、新值等普通参数;两者的边界由 EVAL 的 numkeys 决定。

官方地址:https://redis.io/docs/latest/develop/programmability/eval-intro/

先看 EVAL 的参数边界

EVAL 的基本形式是 EVAL script numkeys [key ...] [arg ...]。脚本字符串之后的第一个数字不是“参数总数”,而是后面有多少个参数属于 key。Redis 从第三个参数开始读取 key 名称,读完 numkeys 个之后,剩下的才会进入普通参数区域。

# numkeys=1:user:1001 属于 KEYS[1],42 属于 ARGV[1]
redis-cli EVAL "return {KEYS[1], ARGV[1]}" 1 user:1001 42

# numkeys=0:没有 key,后面的文本全部是普通参数
redis-cli EVAL "return ARGV[1]" 0 hello

因此,不能把 numkeys 写成“后面一共有几个值”。如果写成 2,Redis 会把接下来的两个参数都放入 KEYS,即使第二个参数本来只是业务状态。

Redis EVAL 参数区间中 numkeys 将 key 参数与普通参数分开的静态结构说明图
图1:EVAL 参数边界说明图,查看 numkeys 如何把 key 名称分配给 KEYS、把其余输入分配给 ARGV;这是静态说明图,不是运行截图。

在 Lua 中分别读取 KEYS 和 ARGV

Redis 会在脚本执行上下文中预先提供两个表。KEYS[1] 到 KEYS[n] 对应显式传入的 key,ARGV[1] 到 ARGV[m] 对应剩余的普通参数。下标从 1 开始,不是 Go、JavaScript 那种从 0 开始。

-- KEYS[1] 是要读取的 key,ARGV[1] 是业务传入的最低分数
local current = redis.call('GET', KEYS[1])
local minimum = tonumber(ARGV[1])

-- key 不存在时返回明确结果,避免把 nil 当成普通数字比较
if not current then
    return {0, 'missing'}
end

-- 只把业务参数用于判断,不把它拼进 key 名称
if tonumber(current) >= minimum then
    return {1, current}
end
return {0, current}

这段代码里的职责是分开的:KEYS[1] 表示数据位置,ARGV[1] 表示判断条件。普通参数不要拼接成 key 再访问,否则既破坏了参数约定,也会让 Redis Cluster 无法提前知道脚本可能访问哪些 key。

用 redis-cli --eval 对照调用顺序

如果脚本放在 check-score.lua 中,redis-cli --eval 使用逗号分隔 key 区域和普通参数区域。逗号前的参数会进入 KEYS,逗号后的参数会进入 ARGV,这和 EVAL 的 numkeys 规则是同一件事的命令行写法。

# 第一个参数是 KEYS[1];逗号后才是 ARGV[1]
redis-cli --eval check-score.lua account:1001 , 60

# 两个 key 要全部放在逗号前,普通阈值仍放在逗号后
redis-cli --eval compare.lua account:1001 account:1002 , 10

排查时可以先让脚本临时返回参数内容,确认边界后再换回实际逻辑。不要把日志、调试字符串或普通状态值放在 key 区域来“凑数量”,那会把错误隐藏到调用端。

在 Go 客户端固定参数职责

客户端封装最好把 key 和普通参数拆成两个明确的切片。以 go-redis 风格为例,Keys 切片会对应 KEYS,Args 切片会对应 ARGV;具体方法签名请按项目依赖版本调整。

// EvalScore 把 key 与普通阈值分开传给 Redis Lua 脚本。
func EvalScore(ctx context.Context, rdb *redis.Client, key string, minimum int64) (string, error) {
    const script = `
-- KEYS[1] 只表示数据 key,ARGV[1] 表示业务阈值
local value = redis.call('GET', KEYS[1])
if not value then
    return 'missing'
end
if tonumber(value) >= tonumber(ARGV[1]) then
    return value
end
return 'below'
`

    // Keys 与 Args 分开,避免把 minimum 误当作 key。
    result, err := rdb.Eval(ctx, script, []string{key}, minimum).Result()
    if err != nil {
        // 保留 Redis 返回的错误,让调用方决定重试或记录。
        return "", err
    }
    return result.(string), nil
}

多人协作时,命名比位置更可靠:调用函数时先写 key,再写 minimum;脚本内部只从 KEYS 取数据位置,只从 ARGV 取业务输入。若脚本需要多个 key,就在同一份 key 列表中按固定顺序传入,并在注释中写清每个下标的含义。

Redis Lua 客户端将 Go 的 Keys 和 Args 映射到 Lua KEYS 与 ARGV 的静态关系说明图
图2:客户端参数契约说明图,展示 Go 调用端、EVAL 边界和 Lua 运行时两个参数表的对应关系;这是静态说明图,不是运行截图。

集群场景要守住 key 的边界

Redis 官方文档要求脚本访问的所有 key 名称都作为输入 key 显式提供,脚本不要根据字符串拼接或数据内容临时生成 key。原因是集群需要根据 key 识别路由范围;把 key 藏在 ARGV 或脚本内部,会让调用约定和集群路由都变得不可靠。

输入内容放置位置脚本访问方式典型例子
Redis key 名称KEYS 区域KEYS[1]、KEYS[2]user:1001
阈值、状态、新值ARGV 区域ARGV[1]、ARGV[2]60、enabled
脚本源码EVAL 第一个参数Lua 程序return redis.call(...)

最后可以按三点复查调用:第一,numkeys 是否等于 key 的数量;第二,脚本中的每个 KEYS[i] 是否都能在调用端找到对应位置;第三,业务值是否全部从 ARGV 读取。三点一致后,再考虑脚本本身的返回值和异常处理。

常见问题

KEYS 和 ARGV 的下标从 0 开始吗?

不开始。Redis Lua 脚本中的两个表都按 Lua 习惯从 1 开始访问,例如第一个 key 是 KEYS[1],第一个普通参数是 ARGV[1]。

没有 key 时应该怎样传参?

把 numkeys 设为 0,脚本不要访问 KEYS,所有后续输入都会进入 ARGV。命令行写法则是在 --eval 后直接写脚本,再写逗号和普通参数。

可以把 key 名称放进 ARGV 再拼接吗?

不建议。需要访问的 key 应在调用时显式放入 KEYS,并保持固定顺序;这不仅让脚本更容易读,也符合 Redis Cluster 对脚本 key 输入的要求。

总结

numkeys 是 EVAL 的分界线:它先决定有多少个输入属于 key,Redis 再把这些值放到 KEYS,把剩余值放到 ARGV。调用端把 key 和普通参数分成两组,Lua 端按 1 开始的下标读取,集群场景中不动态生成 key,Redis Lua 脚本的参数契约就会清晰很多。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>