eval比多次get/set更可靠,因为lua脚本在redis服务端原子执行,整个流程在单线程中串行完成,避免了客户端多步调用间被其他请求插入导致的竞态问题。

为什么 EVAL 比多次 GET/SET 更可靠
Redis 的 Lua 脚本在服务端原子执行,避免了客户端多次往返时的竞态问题。比如「先查余额、再扣减、最后写日志」这类逻辑,用 Go 分多步调用 Get 和 Set,中间可能被其他客户端修改数据;而封装进 Lua 后,整个流程在 Redis 单线程里跑完,天然串行。
go-redis 本身不校验脚本内容,但会把 Lua 代码原样发给 Redis 执行,所以你得自己确保语法正确、避免超时、不调用阻塞命令(如 KEYS)。
- 脚本必须纯函数式:不能依赖外部状态,所有数据通过
KEYS和ARGV传入 - Redis 6.0+ 支持
EVALSHA缓存脚本 SHA1,go-redis 用Script.Load+Script.Eval自动处理缓存和重试 - 单个脚本默认最大执行时间 5 秒,超时会返回
ERR Error running script (call to f_...): @user_script: N: user_script: N: 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 0
if current >= tonumber(ARGV[1]) then
return 0
end
redis.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 重启清空了缓存)会 fallback 到EVAL
Script.Eval 返回值怎么解析才不 panic
go-redis 的 Script.Eval 返回 *redis.IntCmd 等类型命令对象,不是原始值。直接调 .Val() 前必须检查 .Err() == nil,否则 panic。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
Lua 脚本返回值会被 Redis 自动转换:nil → nil,number → int64,string → 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 { ... }
本地调试 Lua 脚本的三个低成本办法
别等上线才发现语法错或逻辑 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 脚本保存为 .lua 文件,用
lua命令行解释器加 mockredis.call函数做单元测试(适合纯逻辑验证) - 用 go-redis 的
redis.NewClient(&redis.Options{Addr: "localhost:6379"})连真实 Redis,但 key 加前缀如test:xxx,跑完FLUSHDB - 在脚本开头加
redis.log(redis.LOG_WARNING, "DEBUG: "..json.encode({KEYS, ARGV}))(需 Redis 开启notify-keyspace-events并监听 slowlog)
真正容易被忽略的点是:Lua 脚本里的 redis.call("GET", ...) 在 Redis cluster 模式下,要求所有 KEY 必须落在同一个 slot,否则报 CROSSSLOT Keys in request don't hash to the same slot —— 这种错误不会在单机 Redis 上暴露,必须在集群环境提前验证 key 的 hash tag(比如都加上 {user123} 前缀)。










