Agent 应用开发会员账号
知识目录选择核心方向与细分内容
知识单元 33高级实现约 18 分钟

理解 → 实现 → 排错 → 取舍

Outbox 的原子意图与重复投递

考察双写问题、事务 Outbox、重复投递和恢复扫描。

Outbox消息队列可靠投递

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

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

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

先理解

刚接触这个知识点

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

从核心原理开始 →

再实现

准备把原理写进代码

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

查看代码示例 →

会排错

需要处理故障与条件变化

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

沿问题继续深入 →

能取舍

需要设计或评审方案

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

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

LEARN · PRACTICE · REFLECT

知识学习与个人记录

我的笔记与复习 ↗

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

作答与个人记录

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

核心知识 · Outbox 的原子意图与重复投递

先理解核心原理

先备概念:本地数据库事务、队列交付语义、幂等消费

同一事务提交任务与待投递意图,消除“任务存在但通知永久丢失”的双写窗口;实际投递仍可能重复,消费者必须单独保证业务处理可重入。

双写无法靠顺序消除窗口

先写任务再发消息,数据库成功后进程崩溃,队列永远不知道;先发消息再写任务,消费者可能找不到记录。try/catch 只能处理活着的进程,不能处理突然断电。Outbox 把业务行和待发事件放在同一数据库事务里,二者同时提交或回滚。

投递器替代请求路径完成外部发送

投递器扫描未确认事件,发送队列后标记已投递。发送成功而标记失败会重发,所以稳定 event_id、业务 revision 和消费幂等都是必要设计。Outbox 不把数据库与队列变成同一事务,也不提供外部动作端到端一次效果。事件顺序若影响状态,应按业务对象序号处理,而不是只相信网络到达顺序。

队列收到不代表业务完成

消息 ACK 的含义依队列而异,可能只证明消费者已确认交付;它不能直接证明 Agent 的工具成功。消费方先可靠领取或建立幂等处理记录,再在约定边界 ACK。长期 Agent 任务通常让消息触发持久化运行,而不是持有队列消息直到所有模型步骤完成;终态由运行状态和业务回执确认。

可恢复也要可观察

监测未投递事件年龄、重试次数、任务未调度时间和死信;扫描对账能发现任务与状态偏离。清理以已确认交付、保留期和补投需要为依据,不能按创建时间直接删。验收分别在业务事务回滚、提交后未发、发后未标记以及消费完成未 ACK 杀进程,检查没有丢任务且重复有定义。

用一个问题检查理解

任务已写入数据库,但消息没发到队列,如何避免任务永久丢失?

不能把数据库提交和队列发送当作一个天然原子操作。我会在同一数据库事务中写任务与 Outbox 事件,由独立投递器重试发往队列,消费方用稳定事件或操作 ID 去重。消息可能重复,但已提交任务不会因为发送瞬时失败而永久丢失。还要监测未投递事件年龄和任务卡住情况,用对账扫描发现异常。

实现与取舍

两种直接双写都有窗口

先提交任务再发消息,进程可能在中间宕机,任务永远没人执行;先发消息再提交任务,消费者可能找不到任务,或者事务最后回滚。把“异常时再试一次”写在请求线程里不能覆盖进程消失的情况。需要一份与任务提交同时持久化的待投递事实。

原子写入与异步投递

在同一事务写 runs 和 outbox,事件包含 event_id、run_id、类型、载荷版本与创建时间。投递器领取未发送事件,发送后更新状态。如果发送已成功但状态更新失败,下次会重复发送,因此消费者必须有去重或幂等业务转移。不要在持有长数据库事务时等待模型调用或远端消息确认。

消费与重复处理

消费方按合法状态转移领取任务,重复 event_id 不重复创建逻辑运行。处理业务结果与消费记录可以在同一存储事务中提交;涉及外部副作用仍需业务幂等与回执核对。队列的 ACK 只是投递处理确认,不能替代业务成功状态。顺序敏感任务使用版本或序列检查,迟到事件不能把已完成任务退回排队。

运维验证

注入四个宕机点:事务提交前后、消息发送后、标记已发送前。断言所有已提交任务最终可被发现,重复消息不会重复副作用。监控 Outbox 最老事件年龄、重试次数、死信和运行状态停留时间。补偿扫描需要使用同样幂等键,不能修复丢任务时又制造双执行。

代码示例

事务回滚不会留下孤立任务或事件

SQLite 内存事务演示;不包含真实队列投递器、幂等消费或分布式压测。

import sqlite3

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE runs(id TEXT PRIMARY KEY);
CREATE TABLE outbox(event_id TEXT PRIMARY KEY, run_id TEXT NOT NULL);
""")
def submit(run_id, fail=False):
    with db:
        db.execute("INSERT INTO runs VALUES (?)", (run_id,))
        if fail:
            raise RuntimeError("crash before outbox")
        db.execute("INSERT INTO outbox VALUES (?, ?)", (run_id + ":created", run_id))
try:
    submit("r1", fail=True)
except RuntimeError:
    pass
print("after rollback:", db.execute("SELECT count(*) FROM runs").fetchone()[0],
      db.execute("SELECT count(*) FROM outbox").fetchone()[0])
submit("r2")
print("after commit:", db.execute("SELECT count(*) FROM runs").fetchone()[0],
      db.execute("SELECT count(*) FROM outbox").fetchone()[0])
db.close()

预期输出

after rollback: 0 0
after commit: 1 1

工程推演

场景
面试假设:提交 Agent 任务返回成功,偶发任务一直停在 queued。
设计决策
任务和待投递事件同事务提交,后台重试并扫描滞留记录。
验证目标
已提交任务可恢复,重复投递不创建第二个运行。
适用边界
Outbox 不会自动解决外部工具副作用的 exactly-once。

连续追问与解答

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

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

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

事件是更新而非创建

改变的条件:同对象多个 revision 进入队列

延伸问题:重复去重就能防止旧版本覆盖吗?

推导与参考解答

不能,去重防同一事件重复,但不同事件可能乱序。记录对象 revision,拒绝旧 revision 覆盖新状态;需要逐步处理时按对象序号检测缺口并补取。消费者处理业务状态与记录已处理 ID 尽量在同一本地事务中完成。

保持不变的原理:事件身份与状态顺序是独立维度,幂等不能代替版本校验。

消费者要调用远端写工具

改变的条件:消费事务外发生业务副作用

延伸问题:在 processed_events 插入 ID 后就可以保证一次吗?

推导与参考解答

插 ID 后宕机会漏动作,动作成功后再插会重复。先保存意图,使用稳定 operation_id 进行远端幂等或对账,再确认结果;processed ID 可表示已可靠交给运行状态机,不能虚称远端已完成。没有目标支持就保留未知与人工核实。

保持不变的原理:本地原子意图不能跨越远端事务,副作用仍需独立恢复契约。

易错点

  • 把双写包在 try/catch 就算可靠
  • 依赖队列自动去重解决全部问题
  • 长期事务里等待模型

参考资料

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

检查自己理解到哪一步

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

基础达标
能指出先写库和先发消息各自窗口。
中高级信号
给出事务 Outbox、稳定 ID 和幂等消费。
资深信号
能设计宕机点验证、滞留告警与安全重投。

查看独立示例的校验记录

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

画出任务提交、Outbox 投递和消费的事务边界,标记重复发生点。

展开验收要求与检查点
  • 任务和事件同生共死
  • 重复消息有定义处理
  • 丢失投递可被扫描发现

重点检查

  • 识别数据库和队列双写窗口
  • 同事务写 Outbox
  • 处理至少一次投递与恢复扫描