直接用 redis.client.eval 会出错,是因为它每次传输完整脚本,无法利用 redis 脚本缓存,导致网络开销大、cpu 编译压力高;若后续误用 evalsha 但未预先 script load,则触发 noscript 错误。

为什么直接用 redis.Client.Eval 会出错?
Go 的 github.com/go-redis/redis/v8 客户端默认不校验 Lua 脚本 SHA1 缓存,但 Redis 服务端在重复执行同一脚本时依赖 EVALSHA 提升性能。如果只调用 Eval,每次都会传输完整脚本,网络开销大,且无法利用 Redis 的脚本缓存机制。
常见错误现象:压测时 CPU 和延迟突然升高,Redis info commandstats 显示 eval 调用量远高于 evalsha;或遇到 NOSCRIPT No matching script. Please use EVAL 错误——这说明你先用了 EvalSha,但脚本还没被加载。
- 必须显式调用
Script.Load将 Lua 脚本预加载到 Redis,获取 SHA1 值 -
Script.Eval和Script.EvalSha是配套使用的,不能只用后者 - 脚本中 key 参数必须通过
KEYS[1]访问,不能硬编码;参数通过ARGV[1]传入,顺序要和 Go 代码中keys、args切片严格一致
如何安全地封装一个可重用的 Lua 执行器?
不要在每次请求里拼接字符串生成脚本,也不要在 handler 里直接写 client.Eval。应该把脚本内容、key 数量、参数结构提前固化,用 struct 封装逻辑。
示例:实现一个原子计数器,支持带过期时间的自增
type IncrWithExpire struct {
script *redis.Script
}
<p>func NewIncrWithExpire() *IncrWithExpire {
lua := <code> if redis.call("EXISTS", KEYS[1]) == 0 then redis.call("SET", KEYS[1], ARGV[1]) redis.call("EXPIRE", KEYS[1], ARGV[2]) return tonumber(ARGV[1]) else return redis.call("INCRBY", KEYS[1], ARGV[1]) end </code>
return &IncrWithExpire{
script: redis.NewScript(lua),
}
}</p><p>func (i <em>IncrWithExpire) Run(ctx context.Context, client </em>redis.Client, key string, incr int64, expireSec int64) (int64, error) {
ret, err := i.script.Run(ctx, client, []string{key}, incr, expireSec).Int64()
return ret, err
}</p>
-
redis.NewScript内部会自动计算 SHA1,但不会自动 Load;首次运行前需手动触发Load(如在服务启动时) - struct 中持有
*redis.Script,避免重复解析 Lua 字符串 - 参数类型要匹配:Lua 中
ARGV[1]是 string,所以 Go 传incr会被转成字符串,再由tonumber()转回数字——不如统一用strconv.FormatInt转成字符串传入更可控
并发场景下 Lua 脚本执行失败的典型原因
Redis 是单线程执行 Lua 的,但 Go 微服务是多协程的。看似“原子”,实际仍可能因超时、连接中断、脚本阻塞(比如死循环或耗时 redis.call)导致失败。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
常见现象:context deadline exceeded 或 redis: connection closed,但日志里没看到 Lua 报错;或者部分请求成功、部分返回空值。
- 务必设置合理的
context.WithTimeout,建议不超过 500ms;Lua 脚本本身不应做网络 IO、文件读写或复杂计算 - 不要在 Lua 里调用
redis.call("KEYS", "*")这类全量扫描命令,生产环境会被拒绝或拖慢整个 Redis 实例 - 若脚本返回
nil,Go 端.Int64()会报redis.Nil错误,需用result.Val()+ 类型断言判断,或统一用.Result()接收 interface{}
如何验证 Lua 脚本是否真的被 Redis 缓存?
光看 Go 日志不够。得进 Redis 实例确认脚本是否已注册,以及后续是否走 EVALSHA 路径。
方法一:用 redis-cli 手动检查
redis-cli --raw script exists "your-sha1-here" # 返回 1 表示已缓存
方法二:开启 Redis 慢日志并抓包观察命令类型
CONFIG SET slowlog-log-slower-than 0 SLOWLOG GET 10 # 查看是否有大量 eval,还是 mostly evalsha
-
script flush会清空所有已加载脚本,微服务重启后第一次调用会变慢,这是正常行为 - 不同 Redis 集群节点间脚本不共享,使用集群模式时,每个节点都要单独
Load;go-redis的ClusterClient不会自动广播脚本,需自行遍历节点调用Load - 线上发布新脚本前,建议先用
SCRIPT LOAD测试能否成功,再更新 Go 代码,避免上线后脚本缺失导致大面积失败
脚本管理容易被当成“一次写完就不管”的黑盒,但它的生命周期、版本对齐、集群分发,其实比业务逻辑更难 debug。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










