商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何在 Go语言 微服务中利用 Etcd 实现分布式锁

如何在 Go语言 微服务中利用 Etcd 实现分布式锁

  发布于2026-07-09 阅读(0)

扫一扫,手机访问

直接说结论:etcd 分布式锁必须组合 Lease、Txn、Watch 和有序 key 设计,仅用 Put+WithLease 或 CompareAndSwap 无法保证互斥;因为 Put 不检查 key 是否存在,多个客户端会同时写入成功,需用 Txn 的 Compare(Version, "=", 0) 实现“仅当 key 不存在时写入”,并配合 lease 续期、Watch 前序 key 实现公平唤醒与自动清理。

如何在 Go语言 微服务中利用 Etcd 实现分布式锁

先说几个核心判断:不能用 clientv3.Put 裸写 key,也不能只靠 CompareAndSwap 事务就完事——必须组合 LeaseTxnWatch 和有序 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 协程一直挂着。

本文转载于:https://www.php.cn/faq/2790548.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注