READ · UNDERSTAND · TRANSFER
阅读理解,按需巩固
先沿着原理、问答和迁移案例阅读。需要检查理解时,再切换巩固练习或展开个人记录。
核心知识 · 租约接管与过期执行者的写入隔离
先理解核心原理
先备概念:行级条件更新、租约与时钟、副作用幂等
租约过期只允许别人接管,不能让旧进程停止。新旧执行者并存时,实际提交端必须验证执行代次,才可阻止旧结果覆盖新状态。
活性与安全是两个问题
租约让宕机任务能继续,解决无人执行;旧 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 失联后可以被接管,但旧 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 无法禁止旧请求在远端执行。
连续追问与解答
沿着问题的前提和约束继续向下读。先理解参考解答,再尝试收起答案,用自己的话解释因果和取舍。
第 1 层租约还有效但用户已经取消,提交条件如何扩展?
取得执行权之后,用户取消也能改变可提交条件。
参考解答
条件提交除 owner、token、lease_until 外,还检查 run 状态、cancel_epoch 或输入 revision。取消与提交必须在可信存储按定义排序:取消先成功就拒绝后续提交;已经提交的外部动作进入对账而非消失。不要只停止调度,仍允许正在跑的 Worker 写 done。
沿着这个回答继续深入
第 2 层取消先写入了数据库,旧 Worker 仍发出远端请求,能只拒绝最终提交吗?
父问扩展本地条件后,远端动作仍可能越过该边界。
参考解答
不能把本地终态拒绝当作远端没发生。调用发送网关再次检查取消版本和 token,目标支持时按代次或操作键验证;已经发送的请求保存为待核对。产品终态区分取消请求、停止新增动作和已发生效果,避免给出虚假的全回滚。
沿着这个回答继续深入
第 3 层远端不识别 token,幂等键能完全替代 fencing 吗?
远端防护能力不足时,进一步区分动作身份与执行权身份。
参考解答
不能。幂等键保护同一个操作重复,旧 Worker 若生成不同动作或参数仍可能写入。可固定已批准意图,用可信网关按当前代次只派发允许动作,或采用目标业务版本条件。缺少这些能力时只能限制影响和对账,明确无法阻止全部过期副作用。
第 1 层每个批次要不要增加 fence,和领取代次有什么区别?
防旧执行者之外,区分执行代次与业务进度。
参考解答
fence 通常随执行权重新领取或接管增加,表达代次;同一 Worker 的连续批次可用独立 step 或 revision 记录进度。每批加 fence 是一种更细粒度设计,但必须使下游一致识别,不能任意增号造成自身旧请求被拒。operation_id 则对同一业务动作重试保持稳定。
第 1 层如何在 PostgreSQL 中避免 SELECT 与 UPDATE 之间被其他 Worker 接管?
把抽象原子领取落到数据库并发机制。
参考解答
在同一短事务里 SELECT FOR UPDATE SKIP LOCKED 选可领取行,随后更新 owner、递增 token、设置租约,最后提交并返回 token;或用具有原子条件的 UPDATE RETURNING 方案。不能先无锁 SELECT 再另一次 UPDATE。执行工作在事务之后,提交仍用条件更新验证权利未失效。
举一反三:条件变了,怎样推导?
先找出改变的条件,再判断原方案中哪些前提仍成立。下面的案例是教学推演,便于将原理迁移到新问题。
任务状态本地,文件对象远端
改变的条件:提交会覆盖远端报告文件
延伸问题:数据库拒绝旧 done,为什么文件仍被旧 Worker 覆盖?
推导与参考解答
拒绝发生在副作用之后已太晚。远端写入要使用版本条件、目标端 fencing 或先写不可变版本再由可信提交端切换引用。旧 Worker 可以产生孤立草稿,但不能覆盖当前正式引用;垃圾回收按状态清理。只保护本地行不能保护对象存储。
保持不变的原理:执行权验证必须覆盖实际发生效果的资源端。
长任务租约短
改变的条件:一次模型调用长于租约
延伸问题:把 TTL 改成一小时就安全了吗?
推导与参考解答
只能降低正常超时,失联接管也变慢,暂停仍可超一小时。采用独立续约心跳并失权停止新动作,提交端继续检查 token。无法中断已发出的写调用时,仍靠目标幂等或条件提交,不能由 TTL 推出安全。
保持不变的原理:租约长度调活性和容错速度,旧执行者安全靠提交校验。
易错点
- 认为 TTL 过期会杀死旧 Worker
- token 随机生成而不是单调递增
- 只在 Worker 本地检查 token
- 把整个长任务放在数据库锁事务里
参考资料
依据公开技术资料设计;参考资料支持技术机制,场景与评分标准为本站设计,不代表某公司面试原题。 新增问答与迁移案例用于原理讲解,来源核查与案例运行验证分别记录。
检查自己理解到哪一步
读完后可以对照这些标准解释原理、边界和取舍。掌握程度由你自评;需要进一步验证时,再完成下方小任务。
- 基础达标
- 知道租约过期不代表旧 Worker 已停止。
- 中高级信号
- 用递增令牌和条件更新拒绝陈旧写入。
- 资深信号
- 说明各个资源是否支持令牌,以及不支持外部系统的补充控制。
巩固练习 按需完成 · 建议 15 分钟
给旧 Worker 暂停再恢复的时序,标出哪些写入应拒绝。
展开验收要求与检查点
- 新持有者有更高代次
- 存储端执行条件检查
- 不能只靠客户端判断租约
重点检查
- 能复现租约过期后的旧 Worker 复活问题
- 能设计原子领取、续约和受条件约束的提交
- 能明确 fencing 的检查必须由资源写入方执行
- 能区分任务抢占与副作用幂等