Agent 应用开发会员账号
知识目录选择核心方向与细分内容
知识单元 31高级场景设计约 15 分钟

理解 → 实现 → 排错 → 取舍

租约接管与过期执行者的写入隔离

租约负责接管,单调递增令牌负责拒绝过期写入,业务副作用仍须独立约束。

Worker租约Fencing Token并发控制

知识内容核对 2026-10-03 · 原题来源核对 2026-10-02

这个知识点,你想学到哪一步?

按当前基础选择起点,也可以依次深入。遇到不熟悉的概念,先回到核心原理;完成后用知识练习检查理解。

先理解

刚接触这个知识点

补齐先备概念,读原理与反例,再用自己的话解释为什么。

从核心原理开始 →

再实现

准备把原理写进代码

理解实现步骤与边界,完成小任务,对照验收要求检查结果。

查看代码示例 →

会排错

需要处理故障与条件变化

沿连续追问定位失效前提,再比较迁移案例,说明方案应如何调整。

沿问题继续深入 →

能取舍

需要设计或评审方案

结合工程推演与资深自评标准,解释方案的适用条件、代价和替代选择。

分析工程场景 →
知识单元目录

LEARN · PRACTICE · REFLECT

知识学习与个人记录

我的笔记与复习 ↗

先沿着原理、问答和迁移案例阅读。需要检查理解时,再切换巩固练习或展开个人记录。

作答与个人记录

每次修改后的提交会保留为独立历史。掌握程度由你对照标准自评。

核心知识 · 租约接管与过期执行者的写入隔离

先理解核心原理

先备概念:行级条件更新、租约与时钟、副作用幂等

租约过期只允许别人接管,不能让旧进程停止。新旧执行者并存时,实际提交端必须验证执行代次,才可阻止旧结果覆盖新状态。

活性与安全是两个问题

租约让宕机任务能继续,解决无人执行;旧 Worker 可能只是暂停或网络隔离,租约过期后仍恢复运行,带着旧计算提交。延长 TTL 减少接管频率,却不能证明旧进程消失。Redis 文档也提醒对长操作使用 fencing,不能把“进程还活着”当作锁仍有效。

让代次进入提交契约

领取时原子产生单调 fencing token,本地提交同时检查 owner、token、租约和任务状态。续约也须匹配当前 token,旧 Worker 不得续上新租约。token 与操作 ID 不同:前者表达执行权代次,后者表达业务动作身份;接管后同一动作的幂等键不应改变。

检查必须发生在资源端

只在调用前读一次租约会有检查到写入的窗口。本地数据库可用同一条条件 UPDATE 完成验证与提交。外部目标若只接受幂等键,只能减少同一动作重复,不能自动防止过期 Worker 提交另一项写入。fencing 的最高已见 token 方案也只拒绝低于已登记代次的请求;要求立即拒绝旧写时,接管要让目标登记新代次或由可信网关原子校验执行权。

不持锁等待整个 Agent

任务领取用短事务,持有行锁更新 owner 和 lease 后就释放;模型推理和网络调用不放在长事务里。PostgreSQL 的 SKIP LOCKED 适合多消费者领取队列表,但不是一般查询的一致视图。验收暂停旧 Worker、允许新 Worker 接管、再唤醒旧 Worker,检查旧提交在所有目标端被拒绝,而不只看任务表。

用一个问题检查理解

两个 Worker 同时恢复一个 Agent 任务,如何用租约和 Fencing Token 防止旧 Worker 写入?

租约让任务在 Worker 失联后可以被接管,但旧 Worker 可能只是暂停,并在租约过期后重新运行。每次领取任务时原子增加 fencing token;续约和提交必须匹配 owner、token、有效期,旧 token 的写入被拒绝。数据库任务领取可用短事务和行锁,避免执行整段 Agent 时持有锁。外部工具也要识别 token 或稳定幂等键,否则只能防住本地状态覆盖,防不住远端副作用。

实现与取舍

租约过期不代表旧进程已经停止

Worker A 领取任务,随后发生长时间暂停。调度器看到心跳过期,将任务交给 B;A 恢复后仍可能继续提交结果。这是双执行问题,不能用“我们设置了 Redis 锁”结束回答。租约主要提供失联后的可接管性。Redis 官方分布式锁文档也提示长耗时任务需要 fencing token,且不能假设进程活着就一直持有锁。安全目标应写成:新一轮领取已经成立之后,旧轮次不能修改受保护资源。

原子领取生成单调递增的执行代次

任务表设计 run_id、owner、lease_until、fence、status。领取在数据库短事务中完成,条件是待领取或运行中租约已过期;cancelled 和 failed 等终态不能被自动接管;成功后 fence 加一并返回这一代令牌。PostgreSQL 的 FOR UPDATE SKIP LOCKED 适合多个消费者领取队列任务,但它提供的是跳过已锁行的行为,并不会自动产生租约或解决旧进程复活。续约必须同时匹配 owner 和 fence;更新失败立即停止发起新工具调用。不要领取后运行十分钟才提交领取事务,否则会长期占锁且影响接管。

拒绝过期写入要落在最终资源处

结果提交的 UPDATE 需要 run_id、fence、owner、未过期租约等条件,影响零行表示已失去执行权。令牌检查和写入必须同一个事务或同一个原子操作完成,先 SELECT 再无条件 UPDATE 会留下竞态。对外部工单系统,只给请求加一个 token 字段还不够,目标写入方必须验证代次;若无法支持,采用业务唯一键幂等与对账,限制旧 Worker 可造成的影响。批量写入也要逐批检查,长时间 CPU 计算可继续结束,但最终提交仍受约束。

用失联后复活的场景证明设计

验收顺序是 A 领取 fence=1,暂停到过期,B 领取 fence=2,再让 A 提交。要求 A 更新零行,B 能提交。再测旧 owner 续约、新旧 Worker 同时领取、时钟偏移、重试提交及取消后的完成请求。生产判断租约最好统一使用数据库时间,心跳失败保守停止写入;不能把所有节点机器的墙钟当成完全一致。示例使用注入的逻辑时间演示拒绝旧提交,未模拟网络分区、实际行锁或完整集群。

代码示例

SQLite 演示过期代次无法完成任务

Python 标准库可运行,只演示受条件约束的本地提交;逻辑时间由测试注入,生产使用统一时间源并实现续约。

import sqlite3
c = sqlite3.connect(':memory:')
c.execute('CREATE TABLE jobs(id TEXT PRIMARY KEY, owner TEXT, until INTEGER, fence INTEGER, state TEXT)')
c.execute("INSERT INTO jobs VALUES ('r', '', 0, 0, 'ready')")
def claim(owner, now):
    with c:
        cur = c.execute("UPDATE jobs SET owner=?, until=?, fence=fence+1, state='running' WHERE id='r' AND until<=? AND state IN ('ready','running')", (owner, now+5, now))
        if cur.rowcount != 1: return None
        return c.execute("SELECT fence FROM jobs WHERE id='r'").fetchone()[0]
def finish(owner, token, now):
    with c:
        return c.execute("UPDATE jobs SET state='done' WHERE id='r' AND owner=? AND fence=? AND until>? AND state='running'", (owner, token, now)).rowcount
a = claim('A', 0)
b = claim('B', 6)
print(a, b)
print('old:', finish('A', a, 7))
print('new:', finish('B', b, 7))

预期输出

1 2
old: 0
new: 1

工程推演

场景
假设工程场景:长文研究任务被两个 Worker 接管。
设计决策
领取代次递增,文章草稿提交同时验证代次和有效租约;远端操作单独用幂等键。
验证目标
验收目标是旧 Worker 无法覆盖新 Worker 的草稿,且接管状态可追踪。
适用边界
外部资源不检查 token 时,本地 fencing 无法禁止旧请求在远端执行。

连续追问与解答

沿着问题的前提和约束继续向下读。先理解参考解答,再尝试收起答案,用自己的话解释因果和取舍。

举一反三:条件变了,怎样推导?

先找出改变的条件,再判断原方案中哪些前提仍成立。下面的案例是教学推演,便于将原理迁移到新问题。

任务状态本地,文件对象远端

改变的条件:提交会覆盖远端报告文件

延伸问题:数据库拒绝旧 done,为什么文件仍被旧 Worker 覆盖?

推导与参考解答

拒绝发生在副作用之后已太晚。远端写入要使用版本条件、目标端 fencing 或先写不可变版本再由可信提交端切换引用。旧 Worker 可以产生孤立草稿,但不能覆盖当前正式引用;垃圾回收按状态清理。只保护本地行不能保护对象存储。

保持不变的原理:执行权验证必须覆盖实际发生效果的资源端。

长任务租约短

改变的条件:一次模型调用长于租约

延伸问题:把 TTL 改成一小时就安全了吗?

推导与参考解答

只能降低正常超时,失联接管也变慢,暂停仍可超一小时。采用独立续约心跳并失权停止新动作,提交端继续检查 token。无法中断已发出的写调用时,仍靠目标幂等或条件提交,不能由 TTL 推出安全。

保持不变的原理:租约长度调活性和容错速度,旧执行者安全靠提交校验。

易错点

  • 认为 TTL 过期会杀死旧 Worker
  • token 随机生成而不是单调递增
  • 只在 Worker 本地检查 token
  • 把整个长任务放在数据库锁事务里

参考资料

依据公开技术资料设计;参考资料支持技术机制,场景与评分标准为本站设计,不代表某公司面试原题。 新增问答与迁移案例用于原理讲解,来源核查与案例运行验证分别记录。

检查自己理解到哪一步

读完后可以对照这些标准解释原理、边界和取舍。掌握程度由你自评;需要进一步验证时,再完成下方小任务。

基础达标
知道租约过期不代表旧 Worker 已停止。
中高级信号
用递增令牌和条件更新拒绝陈旧写入。
资深信号
说明各个资源是否支持令牌,以及不支持外部系统的补充控制。

查看独立示例的校验记录

继续做进阶研究实验

进程真的退出后,还能怎样继续?

从两份数据库的幂等反例,继续验证租约、检查点和独立服务方回执。解压可靠性实验 v3,在独立目录执行。

阅读全文与故障分析 → · 下载可靠性实验 v3 ↓

python3 cli.py submit --db crash.sqlite
python3 cli.py run --db crash.sqlite --lease-seconds 2 --fault after_collect
python3 cli.py inspect --db crash.sqlite
# 首次运行预期退出码 75;等待至少 2 秒后分别执行
python3 cli.py run --db crash.sqlite
python3 evaluate.py --db crash.sqlite

保留证据,逐条核对

  • 首次 inspect 显示 running,已保存 collect 检查点。
  • 租约过期后 succeeded,generation=2,collect 只提交一次。
  • 按 README 再跑 after_effect,核对独立 publisher 数据库只有一个 receipt。

固定 collect → draft → verify → publish 流程;验证本地持久协议与真实进程退出,不证明任意远程服务恰好一次执行。

动手验证 按需完成 · 建议 15 分钟

给旧 Worker 暂停再恢复的时序,标出哪些写入应拒绝。

展开验收要求与检查点
  • 新持有者有更高代次
  • 存储端执行条件检查
  • 不能只靠客户端判断租约

重点检查

  • 能复现租约过期后的旧 Worker 复活问题
  • 能设计原子领取、续约和受条件约束的提交
  • 能明确 fencing 的检查必须由资源写入方执行
  • 能区分任务抢占与副作用幂等