分布式锁必须用Redis原子命令SET key value NX EX实现,value为唯一UUID,解锁和续期均需Lua脚本校验所有权,长任务需看门狗续期并绑定可取消的context。

直接用 sync.Mutex 或 Gin 自带的中间件做分布式锁,完全无效——它只在单个进程内起作用,10 个服务实例各持一把“假锁”,资源照样被并发写穿。
加锁必须用 SET key value NX EX seconds 原子命令
Redis 的 SET 命令支持一次性完成“不存在才设值 + 设置过期时间 + 值唯一”三件事。漏掉 NX 就可能覆盖别人正在用的锁;只用 SETNX 再配 EXPIRE 是两步操作,中间崩溃会导致锁永久残留。
-
value必须是每个请求独立生成的uuid.NewString(),不能是固定字符串或时间戳(易冲突) -
EX时间建议设为业务 P99 耗时 × 2,比如导出接口通常 1.5s 完成,就设3 * time.Second - Go-redis/v9 的
client.Set(ctx, key, value, ttl)底层会自动转成原子SET ... NX EX,前提是 Redis ≥ 2.6.12 且未禁用 Lua
解锁必须走 Lua 脚本,禁止 GET + DEL
先 GET 判断再 DEL 是经典竞态:A 查到 key 存在、正准备删,B 此刻刚续上锁,A 一删就把 B 的锁干掉了。唯一安全方式是把判断和删除压进一个 Lua 原子脚本里执行。
- 推荐脚本:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - Go 中调用:
unlockScript.Run(ctx, rdb, []string{key}, token).Int64(),返回非 1 就代表解锁失败,需记录日志 - 别把脚本硬编码在函数里,用
//go:embed或const管理,方便审计和灰度替换
长任务必须配看门狗续期,且续期也要校验所有权
锁不能靠超时“自动兜底”——业务还没做完锁就过期,后续请求立刻闯入。必须启动后台 goroutine,在锁过期前定期续期,但续期本身也得防误操作。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 续期脚本同样要 Lua 校验:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end - 续期间隔建议为 TTL / 3(如 TTL=30s,则每 10s 续一次),太密加重 Redis 压力,太疏容易掉锁
- 锁结构体里必须保存
context.CancelFunc,Unlock()时显式调用,否则 goroutine 泄漏,“幽灵续期”会续别人刚拿到的锁
Gin 中集成时,别碰 gin.Context 生命周期外的锁管理
Gin 的 Context 是 request-scoped 的,不能拿它存锁对象或传给后台 goroutine——请求结束,ctx.Done() 触发,但续期 goroutine 若没监听这个信号,就会继续跑下去。
- 加锁成功后,应新建一个带 cancel 的子 context:
lockCtx, cancel := context.WithCancel(context.Background()),并绑定续期 goroutine - 在 handler return 前,务必调用
cancel()和unlockScript.Run(...),两者缺一不可 - 不要试图用 Gin 中间件统一加锁——URL 参数、body 内容、用户身份等锁粒度要素,必须由业务 handler 显式决定,中间件无法安全泛化
最常被忽略的一点:锁的 value 不是“标识用途”,而是“所有权凭证”。从生成、传递、校验到销毁,全程必须保持一致且不可预测。任何环节把它当普通字符串拼接、缓存或日志打印,都可能埋下误删或绕过的隐患。

















