在 gin 中用 redis 实现分布式锁,必须通过持有者校验、自动续期和原子释放三重保障,否则易引发超卖、重复执行或死锁;推荐使用 go-redsync/v4 并绑定请求级 context,或手写 lua 脚本确保释放原子性。

直接说结论:在 Gin 中用 Redis 实现分布式锁,不能靠全局变量存锁对象,也不能靠简单 SETNX + DEL 就完事。核心难点是锁的持有者校验、自动续期、以及释放时的原子性——这三个环节任一出错,都会导致超卖、重复执行或死锁。
为什么 SetNX + Del 在 Gin 里大概率失效
常见写法是:进请求 → rdb.SetNX(ctx, "order:123", uuid, time.Second*30) → 成功就执行业务 → 最后 rdb.Del(ctx, "order:123")。问题在于:
-
Del没做持有者校验,任何请求都能删掉别人的锁(比如 A 拿到锁但卡住,B 超时重试也拿到锁,A 恢复后删掉 B 的锁) - 业务耗时超过 30 秒,锁自动过期,B 进来,A 和 B 同时操作库存
- Gin 的 handler 是并发执行的,
uuid若声明为包级变量或结构体字段,多个请求会互相覆盖值
go-redsync/v4 的正确集成姿势
它封装了锁的唯一标识生成、续期、安全释放,但必须配合 context.Context 绑定单次请求生命周期:
- 每次请求新建
redsync.Mutex实例,key 动态拼接(如"lock:order:" + orderId),不要复用 - 调用
mutex.Lock()前传入该请求的c.Request.Context(),不是context.Background() - 务必用
defer mutex.Unlock(),且确保 unlock 发生在业务逻辑结束之后(别被 panic 跳过) - 如果业务可能超时,设
mutex.Expiry(10 * time.Second),并启用看门狗(redsync.WithExpiry(10*time.Second))
示例关键片段:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
func handleOrder(c *gin.Context) {
orderId := c.Param("id")
ctx := c.Request.Context()
mutex := rs.NewMutex(client, "lock:order:"+orderId,
redsync.WithExpiry(10*time.Second),
redsync.WithTries(3),
redsync.WithTimeout(2*time.Second),
)
if err := mutex.Lock(); err != nil {
c.JSON(423, gin.H{"error": "lock failed"})
return
}
defer func() { _ = mutex.Unlock() }()
<pre class="brush:php;toolbar:false;">// 执行减库存等操作
// ...}
自己实现锁时必须加的原子释放逻辑
如果你不用 redsync,而是手写 Lua 脚本释放锁,这是唯一可靠方式:
- 释放锁必须用 Lua,保证「读 key 值 == 自己的 uuid」+「删除」两个动作原子执行
- 脚本长这样:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - Go 里调用:
rdb.Eval(ctx, luaScript, []string{lockKey}, uuid).Result() - 别用
GET+DEL两步走——中间可能被其他请求抢先
容易被忽略的 Context 生命周期陷阱
所有 Redis 操作都依赖 context.Context,但 Gin 的 c.Request.Context() 在 handler 返回或超时时会被 cancel。这意味着:
- 如果锁续期协程用了
context.Background(),它会一直跑,哪怕请求已结束(内存泄漏 + 无效续期) - 若用
c.Request.Context()启动续期 goroutine,需监听其 Done() 信号主动退出 -
redsync内部已处理这点,但自己写的续期逻辑必须显式判断ctx.Err() == context.Canceled
最简验证点:压测时看 Redis 中锁 key 的 TTL 是否稳定维持在设定值附近——波动过大,说明续期没生效或 context 已丢。










