KEYS数组只能放完整key,不能放field或score,因为Redis集群需在执行前静态分析所有操作key以确定slot路由,而field/score不属于key,不参与slot计算,必须通过ARGV传入。

Redis 7.2 中 Lua 脚本能安全操作复合数据结构(如 Hash、Sorted Set、List),但必须严格遵守 KEYS/ARGV 分离、避免跨键操作、禁用全局变量等限制,否则脚本会直接报错或行为不可控。
为什么 KEYS 数组只能放 key,不能放 field 或 score?
Redis 的 Lua 沙箱强制要求所有被操作的 Redis 键名必须显式声明在 EVAL 命令的 KEYS 参数中。这是为了支持集群模式下的路由判断——Redis 需要提前知道脚本会访问哪些 key,才能把请求转发到对应 slot 所在的节点。
如果你在脚本里硬编码 redis.call("HGET", "user:1001", "name"),或者把 field 名拼进字符串再传给 HGET,Redis 无法识别这个 key 是否合法,会在执行前就拒绝:ERR Error running script (call to f_...): @user_script:3: @user_script: 3: Bad key passed to redis.call。
- 正确写法:把完整 key(如
"user:1001")传入KEYS[1],field 名(如"name")作为ARGV[1]传入 - 错误写法:
redis.call("HGET", "user:1001", ARGV[1])——"user:1001"没出现在KEYS列表里 - 更隐蔽的错:
local k = "user:"..ARGV[1]; redis.call("GET", k)—— 动态构造 key,Redis 无法静态分析
处理 Hash 和 Sorted Set 时如何避免类型错误?
Redis 的 Lua 环境不自动转换类型。比如 HGET 返回的是 string,即使存的是数字,你也得手动 tonumber();ZSCORE 返回 nil 表示 member 不存在,直接参与算术运算会触发 attempt to perform arithmetic on a nil value 错误。
常见陷阱包括:
-
redis.call("ZINCRBY", KEYS[1], ARGV[1], ARGV[2])中,如果ARGV[1]是字符串"5",ZINCRBY 能接受;但如果它是nil或空字符串,就会失败 -
redis.call("HGETALL", KEYS[1])返回 table,但它的结构是交替的 key-value({"field1","val1","field2","val2"}),不是 key→value 的 map,遍历时要用pairs()不行,得用for i=1,#res,2 do - 对 Sorted Set 执行
ZRANGE后想取 score,不能直接res[1].score—— 它返回的是纯字符串数组,score 和 member 是分开的两个元素
实操建议:所有从 Redis 读出的值,只要后续要参与计算或比较,先做 tonumber() 并检查是否为 nil;读 HGETALL 后用循环按步长 2 解包;读 ZRANGE ... WITHSCORES 后同样按步长 2 处理。
EVALSHA 在 Redis 7.2 中对长脚本更敏感,怎么稳住?
Redis 7.2 默认启用了更严格的脚本缓存策略,EVALSHA 执行失败时不再自动 fallback 到 EVAL,而是直接报 ERR NOSCRIPT No matching script. Please use EVAL。这在客户端没预热脚本、或主从切换后从节点脚本缓存丢失时特别容易踩坑。
解决方式不是“每次都用 EVAL”,而是:
- 上线前用
SCRIPT LOAD主动加载所有核心脚本,拿到 SHA1 后硬编码到客户端配置中 - 客户端调用前先发
SCRIPT EXISTS <sha1></sha1>,返回 0 就立刻补SCRIPT LOAD,再重试EVALSHA - 避免在脚本里用
os.time()、math.random()等非确定性函数——Redis 7.2 对脚本“确定性”校验更严,这类脚本会被拒绝缓存
一个典型例子:你想用 Lua 实现带过期时间的 Hash 写入,但脚本里写了 redis.call("EXPIRE", KEYS[1], math.random(3600, 7200)),Redis 7.2 会直接拒绝加载,因为 math.random 不可重现。
Hash + Sorted Set 联合更新的原子边界在哪?
你可以在一个 Lua 脚本里同时操作 user:1001(Hash)和 leaderboard:202610(Sorted Set),前提是这两个 key 必须落在同一个 Redis 实例上(单机或集群同 slot)。但要注意:Redis 的原子性只保证“脚本执行期间无其他命令插入”,不保证“事务回滚”。一旦脚本中途出错(比如 ZADD 失败),前面的 HSET 已经生效,无法撤回。
所以真实业务中,关键逻辑顺序必须是:
- 先读所有依赖数据(
HGET,ZSCORE),验证前置条件(如余额充足、库存大于 0) - 验证失败立即
return,不执行任何写操作 - 验证通过后再批量写入,且写入顺序要符合业务约束(例如先扣库存再加订单,不能反过来)
最容易被忽略的一点:Redis 7.2 的 redis.replicate_commands() 默认为 true,但如果你在脚本里调用了 redis.call("EVAL", ...) 嵌套执行另一个脚本,外层脚本的复制行为可能被干扰——这种嵌套在生产环境应绝对避免。


















