最稳妥的redis分布式锁方案是set key value nx px expire_ms原子命令+lua脚本校验释放+唯一token;因setnx+expire非原子易致死锁,del直接删键不安全,get+del非原子易误删他人锁,续期需lua校验ownership并用context控制goroutine生命周期。

直接说结论:用 SET key value NX PX expire_ms 原子命令 + Lua 脚本校验释放 + 唯一 token(非固定字符串),是当前 Go 项目里最稳妥、可落地的 Redis 分布式锁方案。别用 SETNX 单独配 EXPIRE,也别用 DEL 直接删键。
为什么不能只用 SETNX 配 EXPIRE
因为这两步不是原子的。进程在 SETNX 成功后、还没来得及执行 EXPIRE 就崩溃了,这个 key 就永远卡在 Redis 里——没有过期时间,也不会被自动清理,等于锁永久死锁。
Redis 2.6.12+ 后必须用一条命令完成三件事:SET 值、NX 条件、PX 过期毫秒数。Go 中用 go-redis 的 SetNX 方法时,第三个参数要传 time.Duration,底层会自动转成 PX 指令;用 redigo 则需手动拼 SET key val "NX" "PX" 30000。
-
value必须是全局唯一标识,比如uuid.NewString()或fmt.Sprintf("%d-%d", os.Getpid(), time.Now().UnixNano()),不能写死成"1"或"locked" - 过期时间建议设为「业务最长耗时 × 2」,但不超过 30 秒(避免主从延迟导致误释放)
- 如果用
go-redisv8,SetNX(ctx, key, value, ttl)返回bool和error,注意判err == nil且结果为true才算真正拿到锁
释放锁为什么必须用 Lua 脚本
因为 GET + DEL 是两步操作,中间可能被其他协程抢先改写 key 的值。你读到的 value 已不是你设的那个,却还是把锁删了——等于帮别人释放了锁。
正确做法是用一段 Lua 脚本,让 Redis 在服务端原子执行「比对 value → 匹配则删」:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
调用时传入 lockKey 和你自己的 token:
-
go-redis:用Eval方法,script.Load(client).Run(ctx, client, []string{key}, token).Val() -
redigo:用conn.Do("EVAL", script, 1, key, token),返回int64,等于1表示删除成功 - 千万别封装成「先 GET 再 DEL」的函数,这是线上事故高发区
业务耗时不确定时,必须加看门狗续期
比如调第三方 HTTP 接口、批量写数据库、或含人工审核环节,锁 TTL 很容易提前过期。此时不续期,其他 goroutine 就会闯入临界区,造成超卖或脏写。
续期不是无脑延长,而是启动一个独立 goroutine,在锁剩余寿命约 1/3 时尝试更新 TTL:
- 续期命令必须再次校验 ownership,用
EVAL脚本:「若当前 value 匹配,则PEXPIRE」 - 续期失败(返回 0)说明锁已被别人抢走,应立刻
return并中止后续逻辑,不能硬着头皮继续执行 - 用
context.WithCancel控制续期 goroutine 生命周期,主流程结束前调cancel(),防止 goroutine 泄漏 - 别用
SETEX续期——它会覆盖 value,导致后续无法校验所有权
最易被忽略的点:Redis 连接池配置。如果 MaxIdle 太小或 IdleTimeout 过短,锁操作可能因取不到连接而超时失败;更隐蔽的是,连接复用导致不同 goroutine 共享同一 conn,若未及时 Close 或 Reset,可能污染 pipeline 状态。务必确认每个锁操作都用新 Conn 或显式 Do,别依赖连接池自动管理事务上下文。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










