etcd分布式锁必须用租约+事务实现,仅Put key会导致假锁;正确做法是Txn中Compare CreateRevision为0并OpPut绑定租约,租约TTL设为任务最大耗时2–3倍,Watch需前缀监听且独立goroutine消费,key路径应按环境/服务/任务分层设计,释放锁须显式Delete并校验结果。

etcd分布式锁必须用租约+事务,不能只Put一个key
直接client.Put()写入锁key是错的——它不具备原子性,多个客户端并发写会覆盖彼此,导致“假锁”。正确做法是用Txn()配合Compare()判断key是否首次创建:clientv3.Compare(clientv3.CreateRevision(key), "=", 0)。只有当key从未存在过时,才允许写入并绑定租约。否则事务失败,需重试或等待。
- 租约TTL必须大于单次任务执行耗时,建议设为任务最大耗时的2–3倍(如任务最长10秒,租约设30秒)
- 事务中
OpPut()必须带clientv3.WithLease(leaseID),否则key不会随租约自动删除 - 不要用
context.WithTimeout()包裹整个获取锁循环,会导致租约Grant失败后无法重试;应只对单次Grant()和Txn()设超时
Watch前缀监听锁释放事件,但别在回调里直接抢锁
当持有锁的节点崩溃,租约到期后key被自动删除,其他等待节点需要感知并立即竞争。常见错误是把Watch()放在锁获取逻辑里、并在ev.Type == mvccpb.DELETE时立刻调用Txn()——这会造成惊群效应,所有监听者同时发起事务,加剧etcd压力且大概率全部失败。
- 正确做法:Watch只作信号通知,收到DELETE事件后往一个
chan struct{}发信号,由单个goroutine串行处理抢锁逻辑 - Watch必须加
clientv3.WithPrefix(),例如监听/locks/task-scheduler/,避免漏掉同类锁的变更 - Watch返回的
watchChan必须用独立goroutine消费,否则缓冲区满(默认100条)后事件丢失——这是Watch收不到通知的最常见原因
锁Key路径设计影响权限隔离与扩缩容
把所有锁都塞进/lock/global看似简单,但实际会导致三类问题:权限难管控(运维无法限制某服务只能操作自己的锁)、watch范围过大(一次变更触发全量服务重建监听)、扩缩容时冲突概率飙升(新实例启动时集体争抢同一把锁)。
- 推荐结构:
/locks/{env}/{service}/{task-name},例如/locks/prod/order-service/inventory-sync - 任务级锁建议带唯一标识,如
/locks/prod/order-service/inventory-sync/{shard-id},便于水平分片 - 避免在key中硬编码主机名或IP(如
/lock/node-123),节点下线后残留key无法清理;改用UUID或服务实例ID(从注册中心获取) - etcd不支持通配符匹配,所以
Watch("/locks/prod/")能命中/locks/prod/a和/locks/prod/b,但Watch("/locks/prod/*")无效
释放锁必须显式Delete,不能依赖租约自动过期
租约到期自动删key只是兜底机制,不能当作正常释放流程。业务逻辑完成时若不主动Delete(),会导致锁空转占用租约资源,尤其在高频调度场景下易触发etcd连接数或lease数量上限。
立即学习“go语言免费学习笔记(深入)”;
- 释放操作必须用
client.Delete(ctx, key),且需校验返回的resp.Deleted是否为1,防止误删其他key - 不要在defer里放Delete——如果任务panic,defer可能不执行;应在主流程末尾明确调用,并用recover兜住panic后补删
- Cancel租约(
lease.Revoke())不是必须步骤,但建议在Delete后调用,避免etcd后台仍维护已失效租约元数据 - 生产环境务必开启etcd的
--auto-compaction-retention=1h,否则revision堆积会拖慢Watch性能
真正难的不是实现锁逻辑,而是让锁在节点频繁上下线、网络抖动、任务超时重试等真实故障下仍保持“最多一个执行者”的语义。这要求你严格区分租约续期、事务重试、watch重建、key清理四个环节的边界,每个环节都要有独立超时和失败回退策略。
文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/xitongjiaocheng/123891.html