go-redis/v9的SetNX仅解决原子加锁,解锁必须用Lua脚本校验value,长任务需安全续期,否则将导致死锁或误删;value须唯一(如uuid),key需带hash tag确保集群路由一致。

go-redis/v9 的 SetNX 方法能直接实现加锁,但仅靠它远远不够——解锁必须用 Lua 脚本校验 value,长任务必须配续期,否则不是死锁就是误删。
加锁为什么不能只用 rdb.SetNX?
表面上看,rdb.SetNX(ctx, key, val, ttl) 一行就完事,底层确实封装了 SET key val NX EX seconds 原子命令。但它只解决「抢到」的问题,不解决「抢到后挂了怎么办」和「别人能不能删我的锁」。
- 如果业务执行中 panic 或提前 return,
Unlock根本没机会调,锁靠 TTL 自动释放——这没问题;但若 TTL 设得太短(比如只设1 * time.Second),而实际耗时 1.5s,锁提前释放,其他 goroutine 就会闯入 -
SetNX返回false时,很多代码直接忽略,继续往下走,结果以为自己持锁,实则根本没拿到 - value 写死成
"1"或复用字符串,会导致多个 goroutine 拿到同一把“逻辑锁”,解锁时无法区分归属
解锁必须用 Lua 脚本,别信 GET + DEL
这是最常踩的坑:GET 判断值相等,再 DEL,中间有竞态窗口。A 查到 key 是自己的,正要删,B 抢到了锁并写入新 value,A 还是照删——B 的锁瞬间被干掉。
- 正确做法是原子脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:key abc123 - Go 里推荐用
redis.NewScript(unlockScript).Run(...),返回int64;非1就代表失败,得记录日志或告警,不能静默吞掉 - 脚本别硬编码在函数里,用
embed.FS或const管理,方便审计、灰度替换、避免拼错
长任务必须续期,且续期本身要校验所有权
锁不能无限期延长,也不能随便续。TTL 是防死锁的底线,续期是保业务不中断的手段,二者缺一不可。
- 续期命令也得用 Lua:检查当前 key 的 value 是否仍等于自己的 clientID,再
EXPIRE,否则可能续了别人的锁 - 续期间隔建议设为
TTL / 3,比如 TTL=3s,就每 1s 续一次;太密压 Redis,太疏容易掉锁 - 业务函数
return前必须显式 stop 续期 goroutine(如调cancel()),否则锁早释放了,后台还在不停发EXPIRE请求——变成“幽灵续期” - 别依赖
WithAutoRefresh一类封装,它只在锁仍属当前客户端时才生效;你要知道它何时失效、怎么 fallback
Redis Cluster 下 key 必须带 hash tag
在集群模式下,key 默认按 CRC16 散列到不同节点。如果加锁用 order:123,解锁却用 order:123:lock,两个操作可能落到不同节点,导致锁永远拿不到或删不掉。
- 强制路由到同一节点:用
{order:123}:lock这种格式,花括号内内容参与散列,外部被忽略 - 所有相关操作(加锁、解锁、续期)必须用完全一致的 key 名,包括 hash tag
- 别指望客户端自动处理 hash tag,
go-redis/v9不会帮你改 key,你得自己构造
真正难的不是写对第一行 SetNX,而是确保整个生命周期里:锁不被误删、不被漏续、不因网络分区卡死、不因 key 散列错位失效。这些细节藏在重试逻辑、Lua 脚本、hash tag 和 goroutine 生命周期管理里,而不是 API 文档的首页示例中。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











