分布式锁必须依赖redis、etcd或zookeeper等外部存储,sync.mutex仅限单进程;redis需用set nx+ex原子命令、唯一锁值及lua脚本释放;etcd依赖cas+lease事务;zookeeper靠临时顺序节点与watcher实现公平锁。
直接用 sync.mutex 在多实例部署下完全无效——它只锁住当前进程内存,其他 pod、其他服务器上的 go 程序压根感知不到。真正在生产环境能用的分布式锁,必须依赖外部存储的原子能力,目前只有 redis、etcd、zookeeper 三类方案真正成熟。
Redis 实现:别踩 SETNX + EXPIRE 两步坑
很多新手写法是先 SETNX 加锁,再单独调 EXPIRE 设过期时间。这中间一旦进程崩溃或网络中断,锁就永久残留,形成死锁。
- 必须用原子命令:
SET key value EX seconds NX(go-redis/v9 中对应SetNX方法,传入time.Duration即自动封装) - 锁值不能是固定字符串,得是全局唯一标识,比如
uuid.NewString(),否则释放时无法校验所有权 - 释放锁必须走 Lua 脚本,确保「读值比对 + 删除」原子执行,脚本长这样:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - Redis Cluster 模式下,key 必须带 hash tag,例如
{lock:order}:123,否则可能路由到不同节点导致锁失效
Etcd 实现:CAS + Lease 是最稳的起点
Etcd 的事务模型天然适合做锁,服务端保证「检查条件 + 写入」一步完成,不靠客户端拼逻辑。
- 加锁必须用
Txn:先Compare目标 key 的 version 是否为 0(不存在),再OpPut并绑定 lease ID - lease ID 必须由
client.Grant(ctx, ttl)向 Etcd 申请,不能自己生成时间戳或随机数——否则续期和自动清理都失效 - 续租要主动调
client.KeepAliveOnce(ctx, leaseID),且必须处理context.DeadlineExceeded和连接断开错误,失败后应主动Revoke - 抢锁失败时,用
WithPrevKV能直接拿到当前持有者的 value,方便排查谁卡住了锁
ZooKeeper 实现:临时顺序节点 + Watcher 是公平性的保障
ZooKeeper 不需要客户端维护心跳或续期,靠 Session 自动回收临时节点,天生防死锁。
- 每个客户端在父路径下创建临时顺序节点(如
/locks/lock-0000000001),最小序号者获锁 - 未获锁的客户端只监听前一个序号节点(如
lock-0000000001监听lock-0000000000),避免“羊群效应” - Go-Zookeeper 的
Lock结构体已封装好生命周期,但要注意NewLock传入的path必须是已存在的持久节点,否则会 panic - Watcher 回调里不能做耗时操作,建议只触发一次
Lock()重试,否则可能堆积大量 goroutine
三个方案里,Redis 最快但脑裂风险不可消除;Etcd 一致性最强,适合金融级场景;ZooKeeper 公平性最好,但运维成本高。真正容易被忽略的是锁粒度和超时设置——锁 key 必须带业务上下文(如 lock:order:123 而非 lock:order),过期时间得按 P99 业务耗时 × 2~3 设置,而不是拍脑袋填 30 秒。











