Gin中集成分布式锁需用redis.Client.Set原子命令配唯一value和合理TTL,解锁与续期必须用Lua脚本校验value,禁用SetNX和sync.Mutex。

直接在 Gin 中调 client.SetNX 或包一层 sync.Mutex 就算集成分布式锁?不行。多实例部署下 sync.Mutex 完全无效;而 client.SetNX 在旧 Redis(如 2.4)或禁 Lua 环境会 fallback 成两步操作,中间崩溃就导致死锁。
加锁必须用 client.Set + 原子参数
Go-redis/v9 的 client.Set 默认走原子 SET key value NX EX,安全可靠;client.SetNX 则不保证原子性——它底层可能先 SETNX 再 EXPIRE,网络中断或 panic 后 key 永不超时。
- 显式传
redis.WithValue("uuid")和redis.WithExpiration(5 * time.Second),避免依赖默认行为 -
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 是否匹配,再执行
EXPIRE,否则就是跨请求篡改锁 - 看门狗应单独起 goroutine,在锁过期前 1/3 时间触发续期,比如 TTL=10s,则每 3s 续一次
- 续期失败(返回 0 或 error)要立刻中止当前操作,不能静默忽略
- 别让看门狗无限续——设置最大续期次数或总耗时上限,防止任务卡死却一直占锁
最常被忽略的点:TTL 不是拍脑袋定的,它必须基于真实压测的 P99 耗时 × 2;而 value 的唯一性不是“看起来随机”,是每个请求必须重新生成、不可复用、不可预测——这两条没做到,锁就只是个心理安慰。


















