直接用 eval 容易失败,因 go-redis 的 eval 不自动分离 keys/argv,集群下 key 未显式传入 keys 数组会触发 crossslot 错误;且每次重传脚本体,性能差、无 sha 复用,应改用 redis.script 封装后配合 evalsha 与 load。

为什么直接用 EVAL 执行 Lua 锁脚本容易失败
Go-redis 的 EVAL 不自动处理键与参数分离,而 Redis Lua 脚本要求所有 key 必须显式传入 KEYS 数组,否则在集群模式下会报 CROSSSLOT Keys in request don't hash to the same slot。更关键的是,EVAL 无法复用脚本 SHA,每次执行都重新加载 Lua,性能差且易受网络抖动影响。
- 集群环境下必须用
EVALSHA+SCRIPT LOAD组合,或改用EvalSha方法 - 锁脚本里不能依赖
redis.call("get", ...)判断存在性——这会破坏原子性,应统一用redis.call("set", ..., "NX", "PX", ...) - Go-redis v9 默认不缓存脚本 SHA,需手动管理
Script.Load和Script.EvalSha
用 redis.Script 安全封装 SETNX PX 锁逻辑
推荐把 Lua 脚本定义为全局 *redis.Script 实例,调用 Load 一次后复用 SHA。标准加锁脚本应返回 1(成功)或 0(失败),不返回 key 值——避免客户端误判。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
var lockScript = redis.NewScript(`
if redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
`)
// 加锁:key 是锁名,value 是唯一 client_id,expireMs 是毫秒超时
result, err := lockScript.EvalSha(ctx, rdb, []string{"my:lock:key"}, "client_abc123", "30000").Int()
if err != nil {
// 注意:ErrNil 表示脚本未加载,需 fallback 到 Eval
if errors.Is(err, redis.Nil) {
result, _ = lockScript.Eval(ctx, rdb, []string{"my:lock:key"}, "client_abc123", "30000").Int()
}
}
-
KEYS[1]必须是实际锁 key,不能拼接字符串;集群中多个 key 会直接报错 -
ARGV[1]推荐用随机 UUID 或进程+goroutine ID 组合,防止误删他人锁 -
ARGV[2]单位是毫秒,别错写成秒——go-redis 的time.Duration默认纳秒,需转int64(ms)
解锁必须用 Lua 保证原子性,且校验 value
不能用 DEL 直接删 key:可能删掉别人刚续期的锁。必须先 GET 再比对再 DEL,而这三步必须原子执行——只能靠 Lua。
var unlockScript = redis.NewScript(`
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`)
_, err := unlockScript.EvalSha(ctx, rdb, []string{"my:lock:key"}, "client_abc123").Int()
// 返回 1 表示成功删除,0 表示 key 不存在或 value 不匹配
- 解锁时传入的 value 必须和加锁时完全一致,大小写、前缀、编码都不能差
- 如果加锁用了
SET的NX PX,解锁脚本就绝不能用EXPIRE或TTL查询——那些操作本身不原子 - Go-redis v9 中
EvalSha失败会返回redis.Nil,不是nil错误,注意区分
超时自动释放和续期(renew)的实际约束
Redis 锁天然不支持“自动续期”,所谓续期本质是另起一个带新过期时间的 SET,但必须确保旧锁已失效或由同一 client 控制——这又回到 value 校验。生产中建议:锁超时设为业务最大耗时的 2–3 倍,而非依赖续期。
- 续期脚本和解锁脚本结构类似,但用
SET覆盖 value 并重设PX,同样要先校验 value - 不要在 goroutine 里无脑轮询续期:网络延迟可能导致续期请求晚于锁过期,反而覆盖了新持有者的锁
- go-redis 没有内置锁对象,别试图封装成
Lock()/Unlock()隐式续期——隐藏复杂度只会让超时边界更模糊
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










