KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03. 租约、幂等和重复投递怎样一起工作? — keel 龙骨
可靠投递通常是 at-least-once:消息可能重复,worker 可能在外部调用成功后崩溃,旧 worker 还可能在新 worker 接管后迟到。专业系统不假设“恰好一次”,而是让重复和迟到都变成可验证的状态转换。
可靠投递通常是 at-least-once:消息可能重复,worker 可能在外部调用成功后崩溃,旧 worker 还可能在新 worker 接管后迟到。专业系统不假设“恰好一次”,而是让重复和迟到都变成可验证的状态转换。
租约不是锁
Lease 是带过期时间的处理权和 fencing token:
pending --claim(token, until)--> claimed
claimed --ack(token, before until)--> fired | failed
claimed --clock > until--> 可被另一个 worker 接管
确认必须同时匹配 wakeup_id、worker_id、lease_token 和未过期时间。只检查 worker 名称会让旧实例在接管后覆盖新实例的结果。项目的 ack() 对过期 token 明确拒绝,并用测试证明 worker A 的迟到确认不会改变 worker B 的状态。
幂等边界
幂等键要绑定业务意图和参数指纹,而不是随手生成的请求 UUID。事件、外部写操作和通知分别需要自己的 key;一个运行可以有多个投递 attempt,但同一个业务 effect 只能落一次。载荷相同应返回已有结果,载荷不同必须报冲突。
这与 Real-world Execution 中的 fingerprint、receipt、unknown result 和 reconciliation 直接相连:请求超时不能证明外部系统没有执行,必须先进入 unknown 再查询或补偿。
副作用的放置
LangGraph interrupts说明恢复会重新执行节点,因此 interrupt 前的写操作必须幂等。更稳妥的边界是:先持久化意图和 outbox,再执行外部 adapter;外部确认后写 receipt。课程项目用标准库演示状态和去重,生产替换点是事务 outbox、消息 broker 和真正的 fencing/租约服务。
本章实践
- 重复调用
schedule(),验证事件和 wakeup 数量不增加。 - 用两个 worker 领取同一个请求,让第一个 lease 过期。
- 让旧 worker 在接管后 ack,断言收到明确错误且最终状态只出现一次。
- 为一个外部 action 生成两个不同参数的相同 key,断言冲突而不是静默覆盖。
本章检查点
- lease 过期与业务动作完成的先后不确定时,系统如何处理?
- 为什么“数据库唯一键”不能单独证明外部副作用只发生一次?
- 哪些错误可以重试,哪些必须进入人工 reconciliation?