理解 etcd TXN 和 Lease 设计

etcd 经常出现在服务注册、分布式锁、Leader 选举这些场景里。表面上看,业务只是在 etcd 里写入几个 key,再监听这些 key 的变化;但真正让这些场景成立的,不是普通的 PutGet,而是两个更底层的能力:TXNLease

TXN 解决的是 条件原子更新:只有当某个 key 的状态仍然符合预期时,才允许继续写入。Lease 解决的是 状态生命周期:当客户端失联、进程崩溃或者主动退出时,绑定在租约上的 key 能够自动失效。

这篇文章先记录一个设计理解版本,后续再补源码链路。核心结论先放在前面:TXN 决定一个状态能不能被写进去,Lease 决定写进去的状态能活多久。

1. 为什么需要 TXN + Lease#

如果只把 etcd 当作一个分布式 KV,那么服务注册可以非常简单:

PUT /services/order/instance-1 10.0.0.1:8080

服务消费者再监听 /services/order/ 这个前缀,就能得到当前可用实例列表。问题在于,分布式系统里很少只有“写进去”这么简单。

第一个问题是 条件是否仍然成立。比如分布式锁中,客户端只有在锁不存在时才能创建锁;Leader 选举中,候选者只有在自己仍然具备资格时才能确认身份。如果判断和写入被拆成两步,两个客户端可能先后读到相同状态,再同时写入冲突结果。

第二个问题是 状态是否还活着。服务实例可能崩溃,持锁客户端可能卡死,Leader 进程可能失联。如果这些状态只靠客户端主动删除,那么异常退出后就会留下脏数据。

因此,etcd 需要两类能力组合:

  • TXN:把“判断条件”和“执行写入”放进同一次原子操作。
  • Lease:给临时 key 绑定 TTL,让状态随着租约过期自动消失。

2. TXN:把判断和写入放进同一个原子操作#

etcd 的 TXN 可以理解为一个条件表达式:

if Compare:
    Then
else:
    Else

它不是 SQL 意义上的长事务,也不是让客户端开启一个事务上下文后慢慢执行多条语句。它更像一次原子决策:客户端把判断条件、成功分支、失败分支一起交给 etcd,由 etcd 在同一个请求里完成判断和执行。

常见的 Compare 条件包括:

  • version:key 被修改的次数。不存在时通常可用 version == 0 判断。
  • create_revision:key 第一次创建时的全局 revision。
  • mod_revision:key 最近一次修改时的全局 revision。
  • value:key 当前值。

例如“只有锁不存在时才创建锁”,可以表达为:

if version("/locks/job-a") == 0:
    put("/locks/job-a", owner, lease)
else:
    get("/locks/job-a")

这里关键点不在语法,而在语义:version == 0put 不会被其他写入插入打断。判断成立时,写入基于同一个状态完成;判断失败时,客户端也能从 Else 分支拿到当前锁信息,用于决定等待、重试或退出。

TXN 背后还依赖 revision。etcd 的 key 空间每次发生写入,都会推进全局 revision。这个 revision 描述了变化顺序,也让客户端能基于 create_revisionmod_revision 做并发判断。Watch 也依赖 revision 来从某个版本之后继续观察变化。

后续补充:这里还需要继续梳理一次 TXN 请求从 gRPC 进入 etcd,到经过 Raft 提交,再落到 MVCC 存储的完整链路。

3. Lease:给 key 绑定生命周期#

TXN 解决“能不能写”,但不解决“写入后没人清理怎么办”。Lease 负责这个问题。

Lease 的基本流程是:

lease = Grant(ttl)
Put(key, value, lease)
KeepAlive(lease)

客户端先创建一个带 TTL 的租约,再写入 key 时绑定这个租约。只要客户端持续 KeepAlive,租约就会续期;如果客户端崩溃、网络断开或者停止续约,租约最终过期,etcd 会删除绑定在这个 Lease 上的 key。

这使得服务注册非常自然:

Grant(ttl = 10s)
Put("/services/order/instance-1", "10.0.0.1:8080", lease)
KeepAlive(lease)

当实例还活着,它持续续约;当实例消失,租约过期后注册信息自动删除。服务消费者通过 Watch 观察前缀变化,就能感知实例上下线。

需要注意,Lease 不是锁。Lease 只负责生命周期,不负责互斥。一个 Lease 可以绑定多个 key,这些 key 会共享同一组续约和过期行为。是否允许写入某个 key,仍然要靠 TXN 的 Compare 判断。

KeepAlive 也不等于业务状态永远有效。它只能说明某个时间点续约成功。如果 KeepAlive 失败、连接中断或者客户端重连,业务应该重新确认关键 key 是否还存在,不能只靠本地记忆继续执行。

后续补充:这里还需要继续阅读 lessor 的实现,包括过期扫描、Lease checkpoint、租约恢复等细节。

4. TXN + Lease 的组合模式#

4.1 服务注册#

服务注册主要依赖 Lease。

服务实例启动后创建 Lease,把自己的地址写入 /services/<name>/<instance-id>,然后持续 KeepAlive。消费者 Watch 服务前缀,得到实例增删事件。

Grant(ttl)
Put("/services/order/instance-1", address, lease)
KeepAlive(lease)

这里 TXN 不是必选,但在需要防止实例 ID 冲突时可以加入 Compare:只有当 /services/order/instance-1 不存在时才允许注册。这样可以避免两个进程误用同一个 instance id。

if version("/services/order/instance-1") == 0:
    put("/services/order/instance-1", address, lease)
else:
    fail("instance id conflict")

4.2 分布式锁#

分布式锁必须同时依赖 TXN 和 Lease。

TXN 用来保证只有一个客户端能创建锁 key:

if version("/locks/job-a") == 0:
    put("/locks/job-a", owner, lease)
else:
    get("/locks/job-a")

Lease 用来避免死锁。如果持锁客户端崩溃,锁 key 会随着 Lease 过期被删除,其他客户端才有机会继续竞争。

这里也能看出职责边界:TXN 保证“抢锁”动作原子,Lease 保证“持锁者死掉后锁会释放”。缺少 TXN,会出现多个客户端同时认为自己拿到锁;缺少 Lease,会出现客户端崩溃后锁永远不释放。

4.3 Leader 选举#

Leader 选举通常不只是写一个固定 key。更常见的方式是每个候选者写入带 Lease 的唯一 key,再根据 create_revision 或类似顺序判断谁排在最前面。

Grant(ttl)
Put("/election/app/member-1", candidate, lease)
List("/election/app/") by create_revision

排在最前面的候选者成为 Leader。Lease 表示候选者是否还活着;revision 表示候选者进入队列的顺序。Leader 失联后,它的 Lease 过期,对应 key 被删除,后面的候选者通过 Watch 感知变化并重新判断顺序。

这个模式里,TXN 仍然用于关键状态确认,例如确认自己的 key 仍然存在、确认当前观察到的 leader 状态没有被并发变化打断。

5. 边界与误区#

第一,Lease 不是锁。它不会阻止其他客户端写同一个 key,也不会保证互斥。互斥必须来自 TXN Compare。

第二,TXN 不是 SQL 事务。它适合表达一次请求内的条件原子更新,不适合承载跨网络、跨业务步骤的长流程。

第三,KeepAlive 成功不代表业务动作成功。它只说明租约续期成功。业务仍然需要在关键步骤前后确认自己依赖的 key 和 revision 是否符合预期。

第四,Watch 不是可以无限回放的消息队列。Watch 依赖 revision,但历史 revision 可能被 compaction 清理。客户端需要能处理 Watch 中断、revision 被压缩后的恢复逻辑。

6. 后续阅读清单#

这篇先把 TXN 与 Lease 的设计关系写清楚。后续继续补下面几块:

  • MVCC 与 revision 的实现关系。
  • TXN 请求在 etcd server 内部的执行链路。
  • Lease lessor 的过期扫描与 checkpoint 机制。
  • Watch 与 compaction 对客户端恢复逻辑的影响。
  • etcd/client/v3/concurrency 中 Mutex、Election 的封装方式。

小结#

etcd 的分布式协调能力,很多时候来自 TXN 与 Lease 的组合。

TXN 让客户端能把“判断”和“写入”合成一次原子决策,避免并发条件下基于旧状态写入。Lease 让写入的临时状态具备生命周期,避免客户端异常退出后留下脏状态。

因此,理解 etcd 的锁、选举、服务注册时,可以先问两个问题:

  • 这个状态由谁通过什么条件写入?答案通常在 TXN。
  • 这个状态在客户端失联后如何消失?答案通常在 Lease。