如何在 Go语言 微服务中利用 Etcd 实现分布式锁
etcd分布式锁需组合租约、事务、监听和有序键实现。仅用Put+租约无法保证互斥,需事务配合版本号比较实现条件写入,结合租约续期和监听前序键实现公平唤醒与自动清理。持有锁期间必须保持续期,释放锁时需撤销租约,防止死锁。
直接说结论:etcd 分布式锁必须组合 Lease、Txn、Watch 和有序 key 设计,仅用 Put+WithLease 或 CompareAndSwap 无法保证互斥;因为 Put 不检查 key 是否存在,多个客户端会同时写入成功,需用 Txn 的 Compare(Version, "=", 0) 实现“仅当 key 不存在时写入”,并配合 lease 续期、Watch 前序 key 实现公平唤醒与自动清理。

先说几个核心判断:不能用 clientv3.Put 裸写 key,也不能只靠 CompareAndSwap 事务就完事——必须组合 Lease、Txn、Watch 和有序 key 设计,否则两个客户端会同时抢锁成功。
为什么 etcd 的 Put + WithLease 不能直接当锁用
很多团队之前踩过的坑就是下面这种写法:
_, err := client.Put(ctx, "/lock/order", "holder-123", clientv3.WithLease(leaseID))
问题出在哪?根本没做前置检查。多个客户端并发执行这段代码,都会成功写入,然后所有人都以为自己拿到了锁。这不就乱套了。
- etcd 不会自动拒绝重复写入;它只保证单次操作原子性,不保证业务逻辑互斥
WithLease只负责过期清理,不参与竞争判定- 真正需要的是“仅当 key 不存在时才写入”,这必须用
Txn+Compare(Version(key), "=", 0)
加锁必须用 Txn + Compare + WithLease
正确姿势是把“检查 + 写入”打包成一个事务:
txnResp, err := client.Txn(ctx).If(clientv3.Compare(clientv3.Version("/lock/order"), "=", 0)).Then(clientv3.OpPut("/lock/order", "holder-123", clientv3.WithLease(leaseID))).Else(clientv3.OpGet("/lock/order")).Commit()
这里有几个关键细节要注意:
Compare(clientv3.Version(key), "=", 0)表示“该 key 当前 version 是 0”,即从未被创建过Then分支成功才真正写入,且绑定 lease;Else分支可顺便读出当前持有者,方便排查- lease ID 必须由
client.Grant(ctx, ttl)申请,不能手动生成字符串或时间戳——否则续期失败、自动清理失效 - TTL 建议 ≥10s,续期间隔 ≤TTL/3(比如设 15s TTL,则每 5s 续一次)
抢锁失败后,别轮询,要用 Watch 监听前序 key
那么遇到抢锁失败时,应该怎么做?etcd 锁的公平性靠有序 key 实现,比如用时间戳+随机数生成唯一后缀:/lock/order/1678901234567890123。抢锁失败时,可不是睡几百毫秒再试,而是:
- Get 所有
/lock/order/前缀下的 key,按 key 字典序排序 - 找到自己 key 的前一个(比如你的是
/lock/order/1678901234567890123,就监听/lock/order/1678901234567890122) - 调用
client.Watch(ctx, prevKey, clientv3.WithPrevKV(), clientv3.WithRev(revision)),监听其删除事件 - Watch channel 收到响应后,立刻重试抢锁——不是等超时,是“锁一释放就唤醒”
需要警惕的是:Watch 可能断连,必须捕获 context.DeadlineExceeded 或连接错误,并用 WithRev 指定起始 revision 重连,否则漏事件。
持有锁期间必须 KeepAlive,释放锁必须 Revoke
lease 过期不是 key 删除延迟,而是 key 立刻失效。但客户端得主动保活:
- 启动
client.KeepAlive(ctx, leaseID),它返回<-chan *clientv3.LeaseKeepAliveResponse - 在 goroutine 中消费该 channel,收到响应说明续期成功;若 channel 关闭或返回
nil,说明 lease 已过期,应立即放弃锁 - 释放锁不是删 key,而是调
client.Revoke(ctx, leaseID)——etcd 会自动清理所有绑定该 lease 的 key - 别在 defer 里无条件
Revoke:如果锁已被别人抢走,你的Revoke会误删别人的 key
还有一个容易忽略的点:goroutine 泄漏。每个锁实例要绑定自己的 ctx,并在释放锁时 cancel(),否则 KeepAlive 协程一直挂着。


































