加锁必须用 txn + compare,不能裸 put;value 必须全局唯一且带 lease;解锁需 txn.compareanddelete 校验 value;watch 锁释放需处理断连并 resume。

加锁必须用 Txn + Compare,不能裸 Put
裸 Put 返回成功 ≠ 拿到锁。etcd 不保证并发写入互斥,两个客户端同时 Put 同一个 key,都可能返回 nil,导致锁失效。真正安全的做法是用 Txn 做 CAS 校验:Compare(Version(key), "=", 0) 确保 key 从未存在过,再 OpPut 写入带 lease 的唯一 value。
常见错误:先 Get 判断 key 不存在,再 Put —— 中间存在竞态窗口,必然出问题。
-
Version(key) == 0是最可靠的“空闲”判断依据,比Get+IsNotFound更严谨 - value 必须全局唯一,推荐
uuid.NewString()或os.Getpid() + time.Now().UnixNano(),禁用固定字符串如"1" -
Put必须传clientv3.WithLease(leaseID),否则锁不会自动过期
Lease 必须由 etcd 分配并持续保活
lease ID 不能自己拼时间戳或随机数,必须调用 cli.Grant() 获取。自己构造的 ID 无法续期、无法被 etcd 自动回收,等于放弃租约机制。
KeepAlive 不是一次性操作:它返回一个 LeaseResponseChan,你得起 goroutine 持续监听。一旦收到 ErrLeaseExpired,必须立刻放弃锁逻辑,而不是等 context 超时才退出。
- TTL 建议设为
10秒,续租间隔 ≤TTL/3(如3秒),避免 GC 抖动或网络延迟导致误过期 - 不要依赖
Session.Orphan()清理 lease —— 它只在 client 正常 Close 时触发,kill -9或 panic 下完全无效 - 每个锁实例应绑定独立
clientv3.Concurrency.Session,混用 session 会导致心跳冲突和意外释放
解锁必须走 Txn.CompareAndDelete,不能无条件 Delete
解锁不是删 key,而是确认 “这个 key 的 value 确实是我当初设的”。直接 Delete 可能删掉别人刚抢到的锁;defer 里无条件删更危险 —— panic 可能跳过 defer,恢复后又执行一次,造成误删。
正确做法是用 Txn 原子执行:If(Value(key) == myValue).Then(OpDelete())。如果 resp.Succeeded == false,说明锁已被他人持有,此时应直接退出,不可重试或强行覆盖。
- 必须用
WithPrevKV获取当前 value,否则无法做校验 - value 比对必须严格,不能忽略大小写或空格
- 不要复用同一个 key 给不同业务,推荐格式:
/lock/{service}/{resource-id},例如/lock/order-svc/order_123456
Watch 锁释放事件比轮询更高效,但必须处理断连
etcd 的 Watch 能在锁 key 被删除时秒级通知,比 Redis 的固定 sleep 轮询更及时。但 Watch 不可靠:网络抖动、leader 切换都可能导致断连。
断连后必须 resume,且要指定起始 revision(用 clientv3.WithRev(rev)),否则会漏掉中间事件。没 resume 的 Watch 可能无限挂起,导致业务卡死。
- Watch 应配合
context.WithTimeout控制总等待时间,防止单次等待过长 - Watch 返回的
WatchResponse需检查Err()和Events是否为空,不能只看 channel 是否有值 - Watch 到 key 删除后,仍需再次
Txn尝试加锁,不能默认立即成功 —— 可能有其他客户端已抢先写入











