租约失效后为什么还需要 Fencing Token,如何防止旧 Worker 写回?

考查重点:旧持有者隔离与单调令牌。租约到期只让新 Worker 获得资格,旧 Worker 停顿恢复后仍可能写;每次接管发单调递增 Fencing Token,下游拒绝小于已见代次的写入,保护必须由资源端执行

本题目录

参考答案

核心回答

租约到期只表示协调系统可以把执行资格交给新的 Worker,不会让旧 Worker 的线程、计算或在途请求自动消失。旧 Worker 可能在长时间暂停后恢复,继续把过期结果写回;在客户端写前再检查一次租约,也可能在检查与写入之间再次发生暂停。

Fencing Token 给每次新取得的执行资格分配单调递增的代次。所有受保护写入都携带代次,由资源端记录已接受的最大代次,原子地拒绝更旧的请求。这样,新的代次在资源端生效后,旧 Worker 即使恢复运行,也无法用旧代次覆盖新结果。Martin Kleppmann 的分布式锁分析。

一次迟到写怎样被挡住

  1. Worker A 获得代次 41,读取数据后发生长时间暂停。
  2. A 的租约失效,Worker B 获得代次 42。
  3. B 携带 42 写入,资源端保存数据并将最大已接受代次更新为 42。
  4. A 恢复后携带 41 写入,资源端发现 41 < 42,拒绝这次迟到写。

关键在第三、四步的资源端校验。如果资源只记最大已见代次,它在尚未收到新代次之前,并不知道旧租约是否已经到期;这套机制保证的是代次生效后的旧写隔离,而不是向所有资源自动广播租约失效。

实现时要守住哪些条件

  • 代次可靠递增。 新持有者得到的代次要大于旧持有者,协调服务重启或故障切换后也不能回退。随机 UUID 可以区分持有者,却不能表达新旧顺序;普通本地时间戳也不能直接承担这种保证。
  • 比较与写入原子执行。 资源端把代次比较、业务更新和最大代次记录放在同一个条件更新或事务中。若先查询代次,再无条件写入,仍会出现并发空隙。
  • 覆盖实际写入路径。 数据库、对象存储或外部接口必须各自支持相应校验;只在任务表检查一次,不能保护随后无条件发出的外部请求。失去资格后停止后续工作有助于减少浪费,但已经发出的请求仍需要接收端保护。

同一持有者可能在同一代次内执行多次合法写入,因此“拒绝更小代次”与“每次写入都要求代次严格变大”不同;是否允许同代次的重复请求,还要结合操作序号或幂等键设计。Hazelcast 的 Fencing Tokens 说明。

当前尝试校验和幂等键有什么区别

如果任务所有权和结果都在同一个数据库中,也可以保存当前 attempt_id,写回时以 任务标识 + 当前状态 + attempt_id 作为条件,或在锁行事务中检查后保存。接管时替换当前标识,旧标识便无法通过等值校验。这可以隔离该数据库中的旧尝试,但随机标识并不因此变成能被不同下游比较先后的单调 Fencing Token。

幂等键则用于识别“同一个业务操作重试了几次”。同代次的请求仍可能重复,外部调用也可能成功后丢失响应,所以旧写隔离不等于业务只执行一次。接收端不支持代次校验时,应按其能力使用条件版本、幂等键等机制,并明确保护范围。

InterviewGuideJava

基于 InterviewGuide

核心回答

InterviewGuide 的简历分析任务通过数据库中的状态和随机 attemptId 识别当前执行者。消费者原子地把任务从 PENDING 改成 PROCESSING,并记录本次尝试;超时恢复或重试会清除旧尝试,新消费者重新领取时生成新的标识。心跳、完成、失败和重试重置均检查当前尝试,分析结果保存也在数据库事务中验证所有权。

这里采用的是当前尝试的等值校验,能阻止失去执行权的旧消费者写入分析结果或改动新尝试的状态。attemptId 是随机 UUID,并不是面向外部资源的单调递增 Fencing Token;它也无法撤销已经发给模型的分析请求。

怎样领取与恢复任务

AnalyzeStreamConsumer.tryMarkProcessing() 生成 UUID,调用 tryMarkAnalyzeProcessing()。只有数据库中仍为 PENDING 的任务才能更新成功,受影响行数为 1 时才继续分析;重复消息或其他消费者不会仅凭消息到达就取得业务执行权。消费者领取、条件领取。

心跳通过 PROCESSING + analyzeAttemptId 条件更新进展时间,更新失败就抛出执行权失效异常。当前简历分析链在下载解析完成、模型分析返回后报告心跳,并非持续运行独立心跳线程。长调用可能超过无进展阈值,因此阈值需要考虑调用和重试的总耗时。业务阶段与心跳、恢复配置说明。

恢复调度器扫描无进展任务,以状态和时间条件将过期的 PROCESSING 重置为 PENDING、清除旧 attemptId,再补投消息。旧消费者仍可能继续计算,但它持有的旧标识已经无法匹配当前数据库状态。恢复调度、恢复条件更新。

为什么保存前再检查仍然有用

仅靠模型返回后的心跳仍不够:消费者可能刚通过心跳检查,就再次暂停。saveAnalysisIfOwned() 会在事务内通过 findByIdForUpdate() 锁住简历行,重新检查状态和 attemptId,通过后才保存分析结果。这次校验与结果落库受同一个事务保护,恢复调度器不能在二者之间替换任务所有权。锁行查询、事务内验证与结果保存。

完成、失败以及重试重置也带尝试条件。旧消费者失权后,它的重试重置更新不到当前任务,就不会重新投递旧尝试;旧的完成或失败回调也不会覆盖新尝试。条件状态更新、重试处理。

分析结果保存与随后标记 COMPLETED 是两个独立事务,不能把这条链描述为“结果、终态和消息确认一次原子提交”。它保护的是相应数据库写回;模型可能被重复调用,端到端也不具备外部副作用只发生一次的保证。消费执行顺序。

怎样验证旧结果确实被隔离

可以让尝试 A 停在模型调用中,触发恢复并让 B 取得新 attemptId,再释放 A,检查 A 的心跳与结果写回被拒绝;还要覆盖“A 心跳成功后暂停、B 接管、A 再保存”的时序,以验证最终事务检查。分别核对分析记录、任务状态和外部模型调用次数,才能区分旧结果隔离与重复计算。

参考资料

相关面经

3 篇

腾讯

后端开发工程师
日常实习一面
查看原面经 →
拿到租约的节点挂了之后,租约怎么处理?分布式 single‑flight 场景下如何防止旧节点双写?

纷享销客

后端开发工程师
实习
查看原面经 →
当 owner 节点宕机后,你是如何解决"脑裂"导致的状态冲突问题的?

中科星图维天信

后端开发
实习
查看原面经 →
你提到的“分析 Token(令牌)”是怎么解决脑裂的?请详细描述校验流程。
追问
新接管者如何拿到合法操作权?原持有者写结果时如何被拦截?

收录题集

2 个来源,共2 份题集

相关题目

4 题