Redis Cluster 已 SCRIPT LOAD 仍报 NOSCRIPT:缓存不属于整个集群
集群中脚本加载与执行可能落到不同节点。传入真实 KEYS,保留源码与 NOSCRIPT 补载,区分跨槽、流水线和未知执行状态。
Redis Cluster 中,SCRIPT LOAD 已返回 SHA,紧接着 EVALSHA 却报 NOSCRIPT No matching script. Please use EVAL.;直接连接某一节点又能执行。先检查两次命令是否落到了同一个节点,以及应用是否把实际访问的键传给了集群客户端。脚本缓存不是整个集群共享的一份缓存。
这里讨论 Redis Cluster 的 Lua Eval 脚本,代码示例使用 go-redis v9 的接口。老版客户端的参数签名不同,不能把命令行的 numkeys 数字直接照抄为客户端 keys 数组。
SHA 相同,不代表目标节点已经缓存脚本
SHA 标识脚本内容。某个节点返回 SHA,只证明那次加载在接收命令的节点完成;后续命令被路由到另一个节点时,该节点仍可能没有这份缓存。
SCRIPT LOAD 不带业务键,客户端的集群实现如何选择节点,需要按所用版本核对。EVALSHA 则可以根据明确的键参数路由。把键硬编码进 Lua、对外却声明零个键,既不满足键声明规则,也让客户端无法按真正的业务键选择节点。
在受控日志中对应请求实际节点、脚本摘要、客户端版本和键列表。不要记录密码、令牌或完整业务数据。还要区分命令行 EVALSHA digest 0 与 Go 的 []string{"0"}:后者是一个名字为 0 的键,并不是“没有键”。
实际键通过 KEYS 传入,普通值通过 ARGV
脚本访问的所有键都应显式传入,不能在脚本里从其他数据生成未声明的键名。下面是现有 ClusterClient 中读取一个字符串键的示例,ctx、client 和 key 应来自实际应用,保留已有认证、TLS 与超时设置:
script := redis.NewScript(`return redis.call('GET', KEYS[1])`)
value, err := script.Run(ctx, client, []string{key}).Result()
代码需要项目已经导入 github.com/redis/go-redis/v9。key 才是路由所需的实际键;不应把它嵌进脚本文本后传空列表。脚本有业务参数时,将普通值作为 Run 的后续参数传入,在 Lua 中按 ARGV 读取。
当前 Script.Run 先尝试 EVALSHA,遇到 NOSCRIPT 时用同一份源码执行 EVAL。它保留了运行时补载能力;仅保存 SHA、丢掉源码,无法完成这个恢复路径。应核对项目锁定版本确实提供相同行为,不照搬旧接口的调用形状。
多个键还需要满足集群的同槽要求。这是另一项约束:正确声明键并处理缓存缺失,不能使跨槽脚本自动合法;也不能为消除 NOSCRIPT 先给所有业务键改名。
缓存可能在运行期间丢失
脚本缓存不作为数据库数据持久化。节点重启、故障转移或 SCRIPT FLUSH 都可能使目标节点不再认识该摘要,因此启动时加载成功不是以后永远能执行的保证。
Redis 7.4 起,通过 EVAL 或 EVAL_RO 加入的脚本还可能按缓存策略被淘汰。不要把每个请求的值都拼进源码、生成大量不同脚本;使用固定脚本及 KEYS、ARGV 参数。SCRIPT LOAD 的缓存行为与这种 Eval 加载后的淘汰范围应分别理解。
不要通过执行 SCRIPT FLUSH 排查,它会清除其他应用的缓存。诊断可采用受权限控制的 SCRIPT EXISTS,但它只反映被查询节点当时的状态,不能替代应用运行时处理。
流水线中尤其不能在末尾看到 NOSCRIPT 后就重跑整批命令。该批其他写入可能已经完成,重跑会重复执行;应按客户端实际支持的预加载或 EVAL 方案组织流水线,并核对业务幂等性。上述 Run 示例针对普通调用,不能据此保证任意流水线或事务可安全自动重试。
修改前保存客户端调用与脚本版本,在隔离集群检查多个键对应节点及缓存未命中的处理。修复应表现为实际键被正确声明、目标节点按需取得同一脚本、执行结果符合业务要求,而不只是某台节点的命令行能运行。
明确收到 NOSCRIPT 时,目标脚本没有因为这次缓存未命中而执行;网络超时却不能说明脚本没有执行。写脚本的重试应区分这两种结果,不把所有异常都改成无条件 EVAL。回退代码不会撤销已经发生的业务写入,保留数据核对与原部署恢复流程。


