集成分布式锁不能仅用sync.mutex或裸调rdb.setnx;必须用client.set确保原子性,value用uuid,ttl设为p99×2,解锁和续期均需lua脚本校验value并绑定context。

直接用 sync.Mutex 或裸调 rdb.SetNX 就算集成分布式锁?不行。多实例部署下,sync.Mutex 完全失效;而 rdb.SetNX 若底层没走原子 SET key value NX PX,中间一崩溃,锁就永远卡住。
加锁必须用 client.Set,而不是 SetNX
Go-redis/v9 的 client.Set 默认走原子 SET key value NX PX,安全可控;但 client.SetNX 在旧 Redis(如 2.4)或禁 Lua 环境会 fallback 成 SETNX + EXPIRE 两步——中间若 panic、网络中断,key 就永不超时。
- 显式传
redis.WithValue和redis.WithExpiration,确保命令结构可控 -
value必须是每个请求独立生成的,比如uuid.NewString(),不能复用字符串或写死为"1" -
EX用秒级足够,除非你要毫秒精度(那就用PX);注意别把time.Second误乘成纳秒传给EX,否则锁 1 秒变 10⁹ 秒 - TTL 建议设为业务 P99 耗时 × 2,比如导出接口最长 1.2s,就设
2 * time.Second;太短易掉锁,太长拖慢故障恢复
解锁必须走 redis.NewScript + Lua 脚本
用 rdb.Del(ctx, key) 或先 GET 再 DEL 是高危操作:A 拿着锁还没删完,B 已抢到新锁,A 一删就把 B 的锁干掉了——这不是偶发 bug,是必然竞态。
- 解锁脚本固定用:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - Go 中定义:
unlockScript := redis.NewScript(unlockLua),调用时传[]string{key}和value,顺序不能错 - 返回值必须判断是否为
int64(1),0或error都代表失败,此时应立即中止业务逻辑,而不是重试——锁已不属于你 - 脚本别拼在函数里,用
embed.FS或const管理,方便审计和灰度替换
长任务必须配看门狗续期,且续期也需 Lua 校验
锁不能不设 TTL(主从切换后锁丢失),又不能设太长(影响故障恢复),唯一解法是续期——但 A 续了 B 的锁,B 还以为自己持锁在改数据,就完了。
- 续期命令同样要走 Lua,校验当前
value是否匹配再调PEXPIRE,避免“幽灵续期” - 续期 goroutine 必须绑定到单次请求的
context.Context,并随锁释放一起 cancel,否则 GC 前续期仍在跑,可能误续其他请求的锁 - 不要在
defer里直接调releaseLock()而不检查是否真的拿到了锁,否则可能删错 key - 锁 key 应包含业务标识,比如
"pay_lock:" + req.OrderID,而非固定字符串,防重试场景下冲突
最易被忽略的是:TTL 不是随便拍个数,而是必须基于真实 P99 耗时 × 2;value 不是随便生成一个字符串,而是每个请求必须独立 UUID;续期不是“多跑个 goroutine 就行”,而是必须和锁生命周期强绑定、带校验、可 cancel。漏掉任意一点,线上都可能表现为偶发双写或死锁,排查成本远高于预防成本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











