必须使用Txn+Lease闭环实现etcd分布式锁,裸Put不保证互斥性,需CAS校验Version(key)==0再OpPut,value唯一且带leaseID,续租须监听LeaseResponseChan防过期误持。

不能直接用 Put + Delete 模拟锁,必须走 Txn + Lease 闭环,否则节点崩溃后锁永远卡死,或释放时误删别人持有的锁。
加锁必须用 CompareAndSwap 事务,不能只靠 Put 成功判断
etcd 不保证并发写入的互斥性,裸 Put 即使返回 nil 也不代表你拿到了锁——多个客户端可能同时写入成功。真正安全的做法是用 Txn 执行 CAS 检查:Compare(Version(key), "=", 0) 确保 key 不存在,再 OpPut 写入带 leaseID 的值。
-
Version(key) == 0表示 key 从未被创建过;用PrevKV或ModRevision判断已存在时谁在持有,比单纯Get更可靠 -
Put必须传clientv3.WithLease(leaseID),否则锁不会自动过期 - value 必须唯一,推荐
uuid.NewString()或os.Getpid() + time.Now().UnixNano(),禁用固定字符串如"locked" - 不要先
Get再Put:中间窗口期会导致两个客户端都看到 “key 不存在”,然后都成功写入
lease 生命周期必须主动保活,KeepAlive 不能只调一次
Grant 创建的 lease 不会自动续期,客户端需持续调用 KeepAlive 并监听返回的 LeaseResponseChan。一旦收到 ErrLeaseExpired,必须立刻放弃锁,而不是等 context 超时才退出。
- 续租 goroutine 应独立运行,且需处理
context.DeadlineExceeded和网络断连场景 - TTL 建议设为 5–10 秒;续租间隔 ≤ TTL/3(如 1.5s),避免抖动导致过期
- 不要依赖
Session.Orphan()清理:它只在 client 正常关闭时触发,进程 panic 或 kill -9 不生效 - lease ID 必须由 etcd 分配(
cli.Grant()返回),不能自己拼时间戳或随机数——否则无法续期、无法自动回收
解锁必须走 Txn.CompareAndDelete,不能无条件 Delete
释放锁不是删 key,而是确认 “这个 key 的 value 确实是我当初设的” 后再删。etcd 没有原生的 value 比较删除,必须用 Txn().If(cmp.Value("my_value") == true).Then(OpDelete()) 实现原子校验+删除。
立即学习“go语言免费学习笔记(深入)”;
- 错误做法:defer 中无条件
Delete—— panic 可能跳过 defer,恢复后又执行一次,导致删错 - 正确流程:构造
OpGet(key)→ 提取当前 value →Txn.If(Value == myValue).Then(OpDelete()) - 如果
Txn返回resp.Succeeded == false,说明锁已被别人抢走,此时应直接退出,不可重试或强行删 - 使用
clientv3.NewLocker(v3.5+)可省去手写Txn,但它默认共享全局 lease,高并发下易成瓶颈,生产环境建议手动管理 lease
等待锁释放要用 Watch,别轮询
etcd 的 Watch 是事件驱动的,锁一被删就秒级通知,比 Redis 的 sleep+get 轮询更高效、更及时。但 Watch 本身会断连,必须实现 reconnect + resume 逻辑。
- 监听 key 删除事件时,用
WithRev(rev)指定起始 revision,避免漏掉中间变更 - Watch channel 关闭后,要检查
err != nil并重建 watch,不能只靠超时重试 - Watch 不等于“一直等”,必须设最大等待时间(如 30s),否则网络分区时 goroutine 永久挂起
- 业务逻辑拿到锁后,应立即
Watch该 key 的 delete 事件,而非轮询Get—— 这是 etcd 锁区别于 Redis 的关键优势,也是最容易被忽略的性能点
最易被忽略的是 lease 续期失败后的降级处理:没监听 LeaseResponseChan 或忽略 ErrLeaseExpired,会让客户端继续以为自己持锁,实际早已失效。这比加锁逻辑写错更危险——它不报错,只悄悄出错。


















