Redis Lua脚本在Go中必须用Eval或EvalSha实现原子性,不能靠Go加锁模拟;所有需原子性的复合逻辑须打包进单个Lua脚本,keys参数不能为空,返回值需类型断言安全处理。

Redis Lua脚本在 Go 中必须用 Eval 或 EvalSha,不能靠 Go 自己加锁模拟
Go 的 redis.Client(如 github.com/go-redis/redis/v9)不提供“事务+Lua”混合模式;所谓原子性,只来自 Redis 服务端对 Lua 脚本的单线程执行保证。你写个 GET + SET 在 Go 里串行调用,中间照样可能被其他客户端插进来——这不是事务,只是顺序请求。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有需原子性的复合逻辑(比如库存扣减+订单生成+过期时间设置),必须打包进一个 Lua 脚本,通过
client.Eval(ctx, script, keys, args...)一次性提交 - 脚本里用
redis.call("GET", KEYS[1])等原生命令,别试图在 Go 层解析返回再决定下一步——那已经脱离原子边界 - 注意
KEYS和ARGV的分离:键名只能走KEYS数组(Redis 集群强制要求),动态值(如新值、阈值)必须走ARGV
Go 调用 Lua 脚本时,keys 参数不能为空,否则本地测试正常、集群环境直接报错
本地单机 Redis 对 keys 是否为空不敏感,但 Redis Cluster 强制要求每个脚本至少有一个 key,并按 slot 路由。如果你传空 slice 给 keys,go-redis 会静默转成 []interface{}{},Cluster 节点收到后直接返回 ERR Error running script (call to f_...): @user_script:1: @user_script: 1: wrong number of arguments 这类看似参数错、实为 key 缺失的错误。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 即使脚本只读取一个固定 key,也要显式传入
[]string{"order:123"},不能传nil或空 slice - 如果逻辑确实不依赖 key(比如纯计算或访问
redis.call("TIME")),改用EVALSHA+SCRIPT LOAD预加载,但注意预加载脚本在集群中仍需指定任意一个 key 用于路由(可用占位符如"__cluster_routing_key__") - 用
redis-cli --eval测试脚本时,记得补上 dummy key:redis-cli --eval myscript.lua dummykey -- 'arg1' 'arg2'
go-redis/v9 的 Eval 返回值是 Cmd,类型断言稍不注意就 panic
脚本返回 nil、数字、字符串、数组时,cmd.Val() 类型不同:nil 返回 nil,数字是 int64,字符串是 string,数组是 []interface{}。直接强转 cmd.Val().(string) 在返回数字时会 panic,而 cmd.Result() 又会把 nil 转成空字符串,掩盖真实状态。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远先检查
err := cmd.Err(),非 nil 就别碰Val() - 用类型开关解包:
switch v := cmd.Val().(type) { case string: ..., case int64: ..., case []interface{}: ... case nil: ...} - 避免用
Result()处理可能为nil的返回;若需默认值,显式判断v == nil后赋值
复杂逻辑别硬塞进 Lua,Go 层拆解 + Lua 分段执行更可控
一个超过 50 行、含嵌套 if/for、还调用 redis.call 十几次的 Lua 脚本,调试困难、难以单元测试、出错时 Redis 只报 @user_script:27: bad argument 这种无上下文错误。而且 Lua 执行超时(默认 5 秒)会中断整个连接,影响其他命令。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 把“校验”和“变更”分离:先用简单 Lua 检查条件(如
if tonumber(redis.call("GET", KEYS[1])) >= ARGV[1] then return 1 else return 0 end),Go 层根据返回决定是否触发下一段变更脚本 - 涉及多个 key 的操作(如转账),确保所有 key 落在同一 slot(用
{user:1001}这种带 tag 的 key 格式),否则集群下EVAL直接失败 - 对超时敏感的场景(如秒杀),Lua 里避免循环遍历大集合;改用
SSCAN+ 游标分批,或让 Go 层做聚合
真正难的不是写对一行 Eval,而是想清楚哪部分必须塞进 Lua,哪部分可以放回 Go——边界划错,后面全是坑。










