etcd的CompareAndSwap(CAS)不适合直接当分布式锁用,因其仅为事务中的一环,无法保证“加锁→执行→解锁”全流程原子性;两个客户端可能同时CAS成功导致互斥失效,必须结合Lease、有序key、前缀监听与自动续期机制构建完整锁服务。

为什么 etcd 的 CompareAndSwap 不适合直接当分布式锁用
etcd v3 的 CompareAndSwap(CAS)本身不是原子锁原语,它只是 txn(事务)里的一环。直接拿它做锁,容易出现“写入成功但未持有锁”的情况——比如两个客户端同时发起 CAS,都判断 key 不存在然后写入,结果都以为自己抢到了锁。
真正可用的是 etcd 提供的租约(Lease)+ 前缀监听 + 有序 key 的组合方案,核心是靠 Put 的 leaseID 绑定和 Get 的 Sort 排序来实现 FIFO 竞争。
- 必须用
clientv3.LeaseGrant创建带 TTL 的 lease,再把 lock key 绑定到该 lease 上 - lock key 要带自增后缀(如
/lock/my-service/0000000000000001234),靠 etcd 的字典序保证顺序 - 抢锁时先
Put,再Get所有前缀匹配的 key 并按 key 排序,检查自己是不是排第一且 lease 有效 - 释放锁只需
LeaseRevoke,不用删 key——etcd 会自动清理过期 key
用 go.etcd.io/etcd/clientv3 实现可重入、自动续期的锁
官方 clientv3 没有开箱即用的分布式锁封装,得自己组合 Lease、Watch 和 Get。重点不是“加锁”,而是“等锁”和“保锁”:
- 抢锁失败后,要监听比自己小一号的 key(即前一个排队者)的删除事件,而不是轮询
- 持有锁期间必须用
LeaseKeepAlive持续续期,否则 lease 过期锁就丢了 - 锁对象需保存
leaseID、key、client和ctx,避免 goroutine 泄漏 - 重入支持靠本地 map 记录 goroutine ID(如
goroutineID())+ 计数,但注意:跨进程重入不成立,只适用于单实例内多次调用
示例关键逻辑片段:
立即学习“go语言免费学习笔记(深入)”;
// 抢锁时生成带时间戳和随机后缀的 key
key := fmt.Sprintf("/lock/%s/%d-%d", resourceName, time.Now().UnixNano(), rand.Int63())
// Put 时绑定 lease
_, err := client.Put(ctx, key, "", clientv3.WithLease(leaseResp.ID))
常见卡死场景和超时设置怎么配
分布式锁最常卡在“等待前序锁释放”这一步,根本原因不是 etcd 响应慢,而是 Watch 没收到事件或 lease 续期失败。
- Watch 超时默认是永久,但网络抖动会导致连接断开;建议设
clientv3.WithRev()配合重试,而不是依赖单一 watch channel - lease TTL 别设太短(如 3s),否则网络延迟稍高就会误失锁;推荐 ≥10s,续期间隔 ≤TTL/3
- 抢锁总超时(acquire timeout)必须显式控制,否则某个节点挂掉后其他节点会无限等待;建议用
context.WithTimeout包裹整个抢锁流程 - 别忽略
LeaseRevoke的 error:如果 revoke 失败,说明 lease 可能已过期或连接断开,此时锁实际已释放,无需 panic
和 Redis RedLock 对比时要注意的底层差异
etcd 锁强一致性(线性一致性读),Redis RedLock 依赖多节点时钟一致性和多数派写入,两者模型不同,不能简单替换。
- etcd 锁失败只有一种原因:lease 过期或 key 被删;Redis 可能因主从延迟导致“假释放”
- etcd 的
Get带Serializable隔离级别,排序结果绝对可靠;RedisGETRANGE或LRANGE没有内置排序语义,得靠客户端拼 key - 运维上,etcd 集群扩缩容对锁逻辑无感;Redis 集群 slot 变更可能导致 watch channel 断连,需要重连重建
- 如果你的服务已经重度依赖 Redis,硬切 etcd 锁要同步改掉所有锁路径的 error handling 和 timeout 逻辑,尤其注意 context cancel 的传播
真正麻烦的从来不是加锁那几行代码,而是 lease 续期失败时如何安全降级、watch 中断后怎么恢复排队顺序、以及测试时模拟网络分区是否真能触发预期行为。


















