分布式锁仅适用于单次临界操作原子执行场景,跨服务需用saga或事件驱动;redis锁须用set+ex/nx、唯一uuid值、lua脚本解锁、goroutine续期;etcd锁基于raft更可靠;go-redsync需显式设mutex value和expiry;锁使用需严守边界、配对释放、监控指标。

分布式锁不是数据一致性的银弹,它只在「单次临界操作必须原子执行」的场景下有效;跨服务的数据流转,得靠 Saga 或事件驱动,而不是死磕锁。
Redis 分布式锁为什么不能直接用 SETNX
直接调用 SETNX 加锁,三分钟内就可能引发资损:没设过期时间 → 锁永远不释放;没校验持有者 → DEL 误删别人锁;业务执行超时 → 锁提前释放但逻辑还在跑。这三个问题在微服务高频调用下几乎必现。
-
SET必须带EX和NX参数,写成一条原子命令:SET lock:order:123 "uuid-abc" EX 15 NX - 锁值(
"uuid-abc")必须是每个客户端唯一、不可预测的字符串,推荐用uuid.NewString() - 解锁必须用 Lua 脚本,确保「读值比对 + 删除」原子执行:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:123 uuid-abc - 如果业务耗时不确定,得另起 goroutine 调用
PEXPIRE续期,且续期间隔要小于 TTL 的 1/3,否则续不上
etcd 分布式锁更适合强一致性场景
当你已用 etcd 做服务发现或配置中心,顺手拿它实现锁,比 Redis 更可靠——Raft 协议保证线性一致性,没有主从复制延迟导致的锁失效风险。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 抢锁本质是
Put操作,但必须绑定lease:client.Put(ctx, "/locks/order:123", "pod-789", clientv3.WithLease(leaseID)) - 判断是否抢到锁,不能只看
Put返回值,得再Get一次比对 value 是否等于自己的 pod ID - 续期靠
client.KeepAliveOnce(ctx, leaseID),失败需主动Revoke并退出,否则 lease 过期后 key 自动消失,但你的业务可能还在跑 - 释放锁只需
client.Revoke(ctx, leaseID),etcd 自动清理关联 key,不用手写删除逻辑
go-redsync 默认不校验锁持有者
go-redsync 封装了 Redlock 算法,适合多 Redis 实例部署,但它默认的 Unlock() 不做 value 校验,直接 DEL,和裸用 SETNX 一样危险。
- 必须显式传入
redsync.WithMutexValue(uuid.NewString()),让锁实例自带身份标识 - 初始化
mutex时,WithExpiry设为比预估业务耗时长 3 秒以上,否则自动续期来不及触发 - 调用
Unlock()前建议加mutex.IsOwned()判断,避免对已过期锁重复释放(会报redis: nil reply错误) - 若服务启停频繁,
Unlock()可能被调用两次(比如 defer + 显式调用),加IsOwned()是低成本防御
分布式锁最容易被忽略的其实是使用边界
锁只管「同一把钥匙开同一把锁」,不管「钥匙插进去之后门有没有关好」。业务逻辑里漏掉 recover、panic 后没释放锁、context 超时没 cleanup、goroutine 泄漏导致续期协程一直跑——这些比锁算法本身更容易出事。
- 所有
Lock()调用必须配对defer Unlock(),但得包裹在 if 成功分支里,别写在函数开头 - 续期 goroutine 必须监听
ctx.Done(),一收到 cancel 就退出,否则变成僵尸协程 - 锁 key 命名要有业务语义和隔离性,比如
"lock:inventory:sku-1001:shard-3",别用泛化名如"global_lock" - 线上必须暴露锁的持有时长、等待队列长度、失败重试次数等指标,光靠日志很难定位「为什么卡住了」
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










