← 返回题目列表

什么是 Fencing Token?它能解决什么锁问题?

高频 困难 第 14 / 26 题 更新于 2026/10/05
Fencing Token分布式锁租约脑裂
学习 AI 实战项目

简化版

Fencing Token 是锁服务每次成功授予锁时返回的、严格单调递增的编号。下游资源拒绝小于自己已见最大编号的写入,并按接口契约处理等于当前编号的同一持有者操作,因此即使旧持有者因暂停或网络延迟在租约过期后继续执行,也会被下游拒绝。

详细版

租约锁只能证明“某个时刻客户端被授予过锁”,无法阻止已经失去租约的旧客户端继续操作。典型场景是客户端 A 获得 token=41 后发生长时间 GC,锁过期;客户端 B 获得 token=42 并完成写入;A 恢复后再写,如果存储只检查业务请求本身,就可能覆盖 B 的新结果。

正确做法是让锁服务在线性一致的授予点生成 token,并把 token 随每次副作用传给存储。存储原子维护 max_token:小于水位的一律拒绝,大于水位的可推进代次;等于水位的请求只能按已定义的同持有者、多步操作或幂等重试规则处理。它解决的是“迟到旧主写入”,不负责事务回滚、业务幂等,也不能在下游完全无法校验 token 时凭空保证安全。

完整版教学

一、租约为什么仍可能出现两个执行者

租约依赖时间:客户端在期限内拥有资格,超时后服务端可以把锁授予别人。但持锁进程可能被 Stop-The-World、虚拟机暂停、网络分区或调度冻结,它并不知道自己的租约已经失效。恢复后的旧进程与新持有者会在物理时间上同时运行,所以“锁服务没有重复发锁”不等于“资源只收到一个写者”。

易错点:租约负责回收资格,Fencing Token 负责拒绝迟到者;两者解决的是不同层面的安全问题。

二、问题时间线

t0  A acquire -> lease=10s, token=41
t1  A pause 30s
t2  lease expires
t3  B acquire -> token=42 -> storage accepts
t4  A resumes -> token=41 -> storage rejects

如果没有最后一步校验,A 可能根据旧快照覆盖 B 的结果。仅让 A 在写前询问“锁还在吗”也有检查与写入之间的竞态:检查通过后它仍可能暂停,醒来才真正写。

三、Token 必须具备什么性质

token 的关键不是随机唯一,而是与锁授予顺序一致地严格递增。UUID 能区分请求,却无法让存储判断哪个持有者更新;时间戳又受时钟回拨和节点偏差影响。通常由共识日志索引、数据库序列或线性一致计数器生成,并与“锁已授予”在同一原子状态转换中提交。

值能区分持有者能判断新旧适合作 fencing
UUID是否否
客户端时间戳是不可靠否
单调授予序号是是是

四、下游如何执行栅栏校验

存储需把最大 token 与业务写入放在同一原子操作中,避免两个请求都先检查成功再分别写入。关系库可以把版本列放进条件更新;对象存储需要版本前置条件;自研设备或文件服务也必须提供同等语义。

UPDATE account
SET balance = :balance, fence = :token
WHERE id = :id AND fence < :token;
-- affected_rows = 0 时需区分旧 token、同 token 重试或资源不存在

这里假设一次授锁只提交一次最终状态变更,且 fence 是 NOT NULL DEFAULT 0 的持久化列;若允许 NULL,首次比较会得到 UNKNOWN 而无法更新。例如资源已记录 42,迟到的 41 更新影响 0 行;43 可以提交并把水位推进到 43。同一个 token 的请求重试由业务幂等键返回既有结果,不应靠放宽栅栏条件重复执行副作用。

如果同一持有者需要在一次租约内多次合法写同一资源,资源侧应拒绝 token < max_token,并用 owner、操作序号或幂等键约束 token == max_token 的请求;简单把 SQL 改成 fence <= :token 会让重复副作用重新执行。校验必须发生在真正产生副作用的最末端,仅在 API 网关检查后再异步写库仍可能被绕过。

五、Token 的粒度与持久化

全局计数器最容易理解,但可能成为热点;常见做法是每个锁 key 或每类资源维护序号。无论哪种粒度,下游比较的 token 必须与资源归属一致,不能拿订单 A 的 100 去压制订单 B 的 5。水位需要持久化,资源从旧备份恢复后必须保留或抬高它,否则旧 token 会重新变得“合法”。

若一次任务写多个下游,每个下游都要能校验相同代次,或者通过一个具备事务性的权威服务落地。只有部分副作用支持 fencing 时,整体流程仍需幂等、补偿或人工对账。

六、它不能解决哪些问题

Fencing Token 不会自动撤销旧进程已完成的外部调用,也不保证一个任务只执行一次。token=42 的请求自身重试两次,仍需要请求 ID 或唯一约束实现幂等;跨库写到一半失败,仍需要事务、Saga 或对账。若第三方支付接口不接收版本前置条件,只能通过单一代理、幂等键和状态机收窄风险。

它也不替代锁服务的一致性。两个分区若都发出相同或倒退的 token,下游无法建立可靠顺序,因此发号必须建立在线性一致状态上。

七、常见误区与追问

  • 误区:锁设置过期时间就不会有并发持有者。 旧进程可能在租约失效后恢复并继续执行。
  • 误区:给锁 value 放 UUID 就等于 Fencing Token。 UUID 可防误删,但没有可比较的新旧顺序。
  • 误区:写入前再次续约就绝对安全。 续约成功到真正写入之间仍可能暂停或断网。
  • 误区:只在应用层比较 token 就足够。 校验与副作用必须在资源侧原子完成。
  • 追问:token 可以用数据库自增 ID 吗? 可以,前提是授锁与取号顺序一致且故障恢复后不回退。
  • 追问:下游不支持条件写怎么办? 通过支持校验的代理串行化写入,或承认无法获得严格 fencing 保证。
  • 追问:token 溢出如何处理? 预留足够位宽并设计版本迁移,不能回绕后继续比较。

八、加强记忆

记住“租约会过期,进程不会因此自动消失”。锁服务每次授予更大的代次,下游资源只认见过的最大代次,才把迟到旧主真正挡在副作用之前。回答时依次说明暂停时间线、单调 token、资源侧原子条件写,再补充幂等与补偿仍不可少,就能把边界讲完整。