加锁必须原子执行,正确做法是用set key value nx ex命令一步到位;解锁须用lua脚本校验value;长任务需自动续期;锁生命周期须与http请求对齐。

加锁必须原子执行,SET key value NX EX不能拆开
常见错误是先用 Exists 判断 key 是否存在,再 Set + Expire 两步设置。中间任何一步失败(比如网络中断、进程崩溃),就会留下一个没过期时间的死锁 key,后续所有请求都被阻塞。
正确做法是用 Redis 原生命令一步到位:SET product:1001 "uuid-abc" NX EX 15。Gin 路由里调 rdb.SetNX(ctx, key, value, 15*time.Second) 就行——这个方法底层就是封装的原子 SET。别自己拼命令,尤其用 redigo 时参数顺序错一丁点就失效。
-
value必须是全局唯一字符串,推荐uuid.NewString(),严禁写死成"1"或用os.Getpid() -
EX时间设为业务最长耗时再加一点缓冲,比如扣库存+写订单预计最多 8 秒,那就设12秒,别设300秒——锁太久会让故障恢复变慢 - key 命名建议带业务前缀和粒度标识,比如
lock:stock:goods_1001,避免不同功能误删同一 key
解锁必须用 Lua 脚本校验 value,不能直接 Del
直接调 rdb.Del(ctx, key) 是线上事故高发点:A 请求加锁慢,超时释放;B 抢到锁;A 还在执行,最后仍调 Del,就把 B 的锁干掉了。
标准 Lua 脚本只有四行:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end。Go 里要用 redis.NewScript() 预加载,调用时传 []string{key} 和加锁时生成的 clientID(即那个 uuid)。
- 返回
int64(0)表示不是持有者,此时该打 warn 日志,而不是 panic 或重试 - 别在 defer 里无条件删 key——万一没抢到锁,defer 还是会执行,反而可能删掉别人的锁
- Gin handler 里建议把解锁逻辑包进单独函数,确保只在加锁成功后才触发
长任务必须自动续期,keepAlive 不是可选项
库存扣减如果涉及调第三方支付、写多张表、发 MQ,耗时不可控。锁 TTL 到期但业务还没完,另一个实例就会闯入,超卖立刻发生。
续期频率建议设为 TTL 的 1/3(比如 TTL=15s,就每 5s 续一次),用独立 goroutine 执行,绑定 context.WithCancel,主流程结束时调 cancel() 防泄漏。
- 续期本身也要原子:先
GET校验 value 匹配,再PEXPIRE,否则可能把别人刚抢到的锁续上 - 续期 Lua 脚本返回 0,说明锁已丢失,必须立刻中止后续业务逻辑——继续写库或发消息就污染数据
- 别用定时器轮询续期,goroutine + time.Ticker 更轻量可控
Gin 中锁生命周期要和 HTTP 请求生命周期对齐
很多人把锁变量定义在 handler 外部或用全局 map 管理,结果并发请求共享同一个 lock 实例,或者锁对象被 GC 提前回收,导致行为不可预测。
正确做法是在每个 Gin handler 入口生成新锁实例(含唯一 clientID、TTL、续期 goroutine),并在 defer 或 return 前显式释放。如果用了中间件封装锁逻辑,注意 context 传递和 cancel 触发时机。
- 别在中间件里统一加锁然后放行——不同接口锁的 key 粒度不同(用户 ID / 商品 ID / 订单号),硬统一会锁错范围
- 超时控制要分层:HTTP 层设
c.Timeout,Redis 操作设ctx.WithTimeout,锁续期 goroutine 用独立 cancel 控制 - 日志里至少记录:key、clientID、加锁耗时、是否续期、解锁结果——出问题时靠这些定位是锁没生效还是被误删
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











