Go语言本身不提供分布式锁算法,需在Go中调用Redis/etcd等后端,并用goroutine+channel+context实现公平争抢、自动续期和容错;SetNX单独使用因非原子性易致死锁,必须用SET NX PX原子命令;value须为随机字符串,TTL应设为业务P99耗时的2~3倍;释放锁必须用Lua脚本校验value一致性;自动续期不可省略,间隔建议TTL/3;公平性需客户端逻辑保障,如channel串行化或etcd租约。

直接说结论:Go 语言本身不提供分布式锁竞争算法,所谓“通过语言学习实现”是误解;真正要做的,是在 Go 中正确调用 Redis/etcd 等后端,并用 goroutine + channel + context 实现客户端侧的公平争抢、自动续期和容错逻辑。
为什么 SetNX 单独调用不能当分布式锁用
很多人一上来就写 rdb.SetNX(ctx, key, "1", ttl),以为返回 true 就锁住了——这在高并发下极大概率出错。根本问题是:它只保证“不存在才设值”,但没绑定过期时间。一旦进程崩溃或网络中断,这个 key 就永远卡在 Redis 里,形成死锁。
必须用原子命令:SET key value NX PX 30000(毫秒)或 SET key value NX EX 30(秒)。go-redis/v9 的 client.Set(ctx, key, value, ttl) 默认启用 NX+PX,但得确认你连的是 Redis 2.6.12+;老版本会 fallback 成两步操作,中间挂掉就留死锁。
- value 必须是每个 goroutine 独立生成的随机字符串(如
uuid.NewString()),不能复用固定值或时间戳 - ttl 不是拍脑袋定的:应为业务 P99 耗时 × 2~3,例如导出任务 P99 是 1.8s,那就设
4 * time.Second - key 建议带业务上下文前缀,如
lock:order:789,避免跨服务误删
释放锁为什么必须用 Lua 脚本
常见错误是先 GET 判断再 DEL:if rdb.Get(ctx, key).Val() == myValue { rdb.Del(ctx, key) }。这是经典竞态:A 查到锁存在,正要删,B 已抢到并写入新值,A 一删就把 B 的锁干掉了。
立即学习“go语言免费学习笔记(深入)”;
唯一可靠方式是 Lua 脚本原子执行:
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
endGo 中调用:unlockScript.Run(ctx, rdb, []string{key}, lockValue),返回非 1 就代表释放失败,必须记录日志或告警,不能静默忽略。
- 如果返回
redis.Nil,说明 key 不存在或值不匹配——不是成功,是失败 - 别在
defer里无条件调用释放,得先判断自己是否真持有锁(比如通过context.Done()或显式标志)
自动续期不是可选功能,而是保命机制
GC 暂停、网络抖动、磁盘 I/O 延迟都可能让业务执行超过 TTL。不续期 = 锁提前丢失 = 并发写入风险。
续期也得用 Lua:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end- 续期间隔建议为 TTL / 3,比如 TTL=3s,就每 1s 续一次;太密压 Redis,太疏容易掉锁
- 启动续期 goroutine 后,业务函数
return前必须显式cancel()对应 context,否则变成“幽灵续期”——锁早释放了,后台还在给一个不存在的 key 续期
Redis 本身不保证公平性,公平靠客户端逻辑兜底
Redis 没有队列、没有优先级、不记录请求顺序。所谓“多节点公平竞争”,实际是指:谁先发请求、谁先拿到锁,不能因为网络延迟或 Redis 处理顺序导致后发者反而先得锁。
可行做法是加一层客户端排队:
- 用 channel 控制同一把锁的获取请求串行化(适合单实例锁)
- 用 Redis 的
LPUSH + BRPOP模拟等待队列(需额外 key 管理,复杂度上升) - 更稳妥的是换 etcd:它原生支持租约(Lease)+ 事务(Txn)+ 前缀监听,能实现真正的 FIFO 公平锁
最容易被忽略的一点:公平性永远建立在“锁释放后下一个请求立刻能感知并抢占”的前提上。如果业务处理中做了阻塞 IO、长循环或未设 context timeout,那再好的排队逻辑也白搭——锁没丢,只是你卡在自己代码里了。


















