必须用redis.newscript()封装脚本,否则无法触发sha1缓存复用;直接传字符串给eval()会编译失败或panic,因go-redis强制要求*redis.script类型以实现自动sha1计算、本地缓存及evalsha fallback。

必须用 redis.NewScript() 封装脚本,否则无法触发 SHA1 缓存复用,性能提升无从谈起。
为什么直接传字符串给 Eval() 会失效
go-redis 完全屏蔽了裸 EVAL 接口。写 client.Eval(ctx, "return 1", []string{}, nil) 会编译失败或 panic,报错 cannot use string as *redis.Script。这不是限制,而是设计强制你走缓存路径:只有 redis.NewScript() 创建的对象才自带 SHA1 计算、本地缓存、自动 fallback 到 EVALSHA 的能力。手动拼字符串等于放弃所有优化,每次请求都传完整脚本体,网络和 CPU 开销双高。
redis.NewScript() 怎么用才不浪费性能
脚本对象本身是线程安全、可复用的,别在 handler 里反复 new:
- 定义为包级变量,服务启动时初始化一次
- 多行脚本用反引号包裹,避免 Go 字符串转义破坏 Lua 格式(比如注释、缩进)
- 脚本内容变更后,
redis.NewScript()生成的新对象会自动算新 SHA1,旧缓存在 Redis 中不影响,也不需人工清理
示例:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
const luaScript = `
local current = tonumber(redis.call("GET", KEYS[1])) or 0
if current >= tonumber(ARGV[1]) then
return 0
end
redis.call("INCR", KEYS[1])
redis.call("EXPIRE", KEYS[1], ARGV[2])
return 1
`
var rateLimitScript = redis.NewScript(luaScript)
Redis Cluster 场景下必须显式 Load()
普通单节点 Redis,Script.Eval() 内部会先试 EVALSHA,失败再 fallback 到 EVAL,你完全不用管加载逻辑。但 Redis Cluster 要求脚本必须提前存在于所有分片节点,否则可能报 MOVED 或 CROSSSLOT 错误。这时得在服务启动阶段调一次 rateLimitScript.Load(ctx, client):
-
Load()是幂等的,重复调用不会报错,但热路径里调就是浪费网络 - 失败(如某节点宕机)时,
Eval()后续 fallback 仍可能成功,但性能退化成每次发完整脚本 - 拿到的返回值是
[]byte,要转成小写十六进制字符串才能用于调试或监控:fmt.Sprintf("%x", sha)
参数传错类型或空切片会导致脚本内 panic
KEYS 和 ARGV 必须严格区分,且不能为空(除非明确允许):
-
KEYS必须是非空[]string,哪怕只用一个 key,也得写成[]string{"mykey"},否则 Redis 报ERR wrong number of arguments -
ARGV是[]string,数字要传字符串(如"5"),Lua 里再用tonumber()转——传5会变成nil,后续调用直接崩溃 -
Script.Eval()返回的是*redis.IntCmd这类命令对象,不是原始值;必须先检查.Err() == nil才能调.Int(),否则 panic
真正容易被忽略的点是:SHA1 缓存生效的前提,不只是脚本内容不变,还要求 KEYS 和 ARGV 的结构稳定。比如某次传了 2 个 key,另一次只传 1 个,即使脚本一样,Redis 也会认为是不同调用上下文,EVALSHA 可能 fallback —— 这不是 bug,是设计使然。










