发布于2026-07-12 阅读(0)
扫一扫,手机访问
在 Go 语言里操作 Redis,很多时候我们会面临一个选择:到底是一次次地 GET、SET,还是干脆写一段 Lua 脚本一次性搞定?理解这一点,其实就抓住了 Redis 开发中一个关键的效率与安全命题。
为什么说 EVAL 比多次 GET/SET 更可靠?核心在于它解决了“竞态”。设想一个典型的业务场景:先查余额,再扣减,最后写日志。如果用 Go 分三步调用 Get 和 Set,每一步之间都可能被其他客户端插入修改,数据一旦出错,排查起来相当痛苦。但把这三步封装进一个 Lua 脚本,整个流程在 Redis 的单线程里一口气跑完,逻辑自然就串行化了,不存在中间被打断的可能。这其实是用“原子执行”来打补丁,简单直接。
当然,go-redis 本身不校验脚本内容,你得自己保证 Lua 语法正确、不超时,尤其不要在里面调用像 KEYS 这种阻塞命令。有三个要点值得记一下:第一,脚本必须是纯函数式的,所有外部数据一律通过 KEYS 和 ARGV 传入;第二,Redis 6.0+ 支持 EVALSHA 缓存脚本 SHA1,go-redis 的 Script.Load 和 Script.Eval 会自动帮你处理缓存与重试;第三,单个脚本最多跑 5 秒,超时后直接被中断,返回的错误信息里会带有 script tried to execute a disabled command 之类的字样,注意捕获。

redis.Script 安全加载并执行 Lua 脚本不要直接拼接字符串去调用 client.Eval,那样既没法复用 SHA1,也难做错误分类。更好的做法是用 redis.NewScript 把脚本内容封装起来,再调用它的 Eval 或 EvalSha 方法。
还是拿实际场景说话吧——实现一个带过期时间的计数器限流脚本:
const luaScript = `local current = tonumber(redis.call("GET", KEYS[1])) or 0if current >= tonumber(ARGV[1]) then return 0endredis.call("INCR", KEYS[1])redis.call("EXPIRE", KEYS[1], ARGV[2])return 1`rateLimitScript := redis.NewScript(luaScript)result, err := rateLimitScript.Eval(ctx, client, []string{"rate:uid:123"}, "5", "60").Int()
这里有两个容易被忽略的细节:第一,KEYS 必须是非空切片,即使只有一个 key,也得写成 []string{"mykey"},否则 Redis 会直接报 ERR wrong number of arguments。第二,ARGV 是 string 类型切片,数字要转成字符串传入(比如 "5"),然后在 Lua 里通过 tonumber() 转回来。另外,如果脚本之前已经加载过,Eval 内部会自动走 EVALSHA 路径;如果首次执行失败(比如 Redis 重启导致缓存清空),它也会自动回退到 EVAL,省去手动写 fallback 逻辑的麻烦。
Script.Eval 返回值怎么解析才不 panicgo-redis 的 Script.Eval 返回的是 *redis.IntCmd 这类命令对象,不是原始值。如果上来就调 .Val() 而不检查 .Err(),一旦遇到 nil 就会 panic。所以稳妥的做法是先检查错误。
Lua 脚本的返回值会被 Redis 自动映射:nil 对应 nil,数字对应 int64,字符串对应 string,table 对应 []interface{}(嵌套结构需要手动类型断言)。有几个常见的坑:Lua 里用 return 1 / return 0 表示布尔值,Go 侧不能用 .Bool() 解析,得用 .Int() != 0;如果返回数组(比如 return {1, "ok", 3.14}),Go 里需要通过 result.Val().([]interface{}) 拿到数据,再逐个断言类型;返回 nil 时,.Result() 会返回 (nil, nil),此时 .Val() 会 panic,所以前置的 if err := cmd.Err(); err != nil { ... } 判断必不可少。
别等到上线才发现语法错误或逻辑 bug。最直接的方法是本地用 redis-cli --eval 测一下:
redis-cli --eval myscript.lua mykey , 5 60
如果你在 Go 项目里写测试,也可以考虑用 redis.Serve 启动一个 mock server(需要配合 github.com/bsm/ginkgo/v2 和 github.com/bsm/gomega),不过更轻量的做法是:
lua 命令行解释器加一个 mock 的 redis.call 函数来做纯逻辑验证test:xxx 前缀,跑完测试后执行 FLUSHDB 清理redis.log(redis.LOG_WARNING, "DEBUG: "..json.encode({KEYS, ARGV})),然后结合 slowlog 来观察执行过程(前提是 Redis 开启了 notify-keyspace-events)不过话说回来,真正容易被忽略的是:在 Redis Cluster 模式下,Lua 脚本里的所有 KEYS 必须落在同一个 slot 上。如果分散到不同节点,就会报 CROSSSLOT Keys in request don't hash to the same slot。这个错误在单机 Redis 上不会暴露,所以如果项目最终要部署到集群环境,最好提前给 key 加上 hash tag(比如都带上 {user123} 前缀),避免线上出问题。我自己踩过最大的一个坑就是:本地写得挺好,一上集群就报错,查了半天才发现是 slot 的问题。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8