裸put无法保证互斥性,必须用txn做cas校验;因put不校验key是否已被占用,两客户端可能同时写入成功导致“双锁共存”,正确做法是compare version==0后opput绑定etcd分配的leaseid。

裸调用 Put 拿不到真正可用的分布式锁,必须用 Txn 做 CAS 校验;否则两个客户端可能同时写入成功,锁就形同虚设。
为什么不能直接 Put 加锁?
etcd 的 Put 是非原子写入——它不校验目标 key 是否已被占用。两个客户端几乎同时执行 Put,都可能返回 nil 错误,结果就是“双锁共存”,互斥性彻底失效。
常见错误写法:
_, err := client.Put(ctx, "/lock/order", "1", clientv3.WithLease(leaseID))
这行代码只保证写入带租约的值,但完全不管这个 key 是否已被别人抢先占用了。
正确做法是用 Txn 执行原子比较再写入:
-
Compare条件必须是Version("/lock/order") == 0,这是判断 key “从未存在过”最可靠的依据(比先Get再判断IsNotFound更严谨,后者有竞态窗口) -
Then分支执行OpPut,且必须传clientv3.WithLease(leaseID) -
Else分支可返回失败或重试逻辑
leaseID 必须由 etcd 分配,不能手拼
租约不是字符串或时间戳,而是 etcd 内部管理的整型 ID。自己构造 leaseID(比如拼接时间戳、随机数)会导致:
- 无法续期:
KeepAlive只接受真实分配的 ID - 无法自动回收:TTL 到期后 key 不会自动删除,锁永久残留
- 心跳冲突:多个锁实例混用同一个
leaseID,一个释放会连带干掉其他锁
务必通过 client.Grant(ctx, ttl) 获取,例如:
leaseResp, err := client.Grant(ctx, 10) // 10秒TTL
if err != nil { /* handle */ }
leaseID := leaseResp.ID
TTL 建议设为 10 秒,续租间隔 ≤ 3 秒(即 TTL/3),避免 GC 抖动或网络延迟导致误过期。
解锁必须校验 value,不能无条件 Delete
解锁的本质是:“确认这个 key 的 value 确实是我当初设的”。直接 Delete 或在 defer 里无条件删,风险极高:
- 删错人:A 拿到锁后处理超时,B 已抢到新锁,A 的
Delete会把 B 的锁干掉 - 重复删:panic 跳过
defer,恢复后又执行一次Delete,造成二次误删
正确方式是用 Txn.CompareAndDelete:
txn := client.Txn(ctx).
If(clientv3.Compare(clientv3.Value("/lock/order"), "==", myValue)).
Then(clientv3.OpDelete("/lock/order"))
resp, err := txn.Commit()
其中 myValue 必须全局唯一(推荐 uuid.NewString() 或 os.Getpid() + time.Now().UnixNano()),不能用固定字符串如 "1"。
Watch 锁释放时必须处理断连并 resume
如果业务需要监听锁被谁释放(比如做锁抢占或状态同步),要用 client.Watch 监听 key。但 etcd 的 watch 连接不稳定,网络抖动或服务端重启都会导致 channel 关闭。
不能假设一次 Watch 能持续到底。必须:
- 检查
WatchChan是否关闭(ok == false) - 捕获
ErrCompacted或ErrCanceled等错误 - 在出错后重新发起
Watch,并带上上次收到的resp.Header.Revision作为起始 revision,避免漏事件
最易忽略的一点:租约过期后,watch 不会主动通知“锁已释放”,而是等你下一次 Get 或 watch 到 key 被删——所以 watch 的逻辑必须和租约保活、锁重试耦合起来,不能孤立看待。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











