LLM 面试备考
首页/分布式与可靠性

事务、WAL 与补偿(Transaction, WAL & Saga)

所属:01-fundamentals / 07-distributed-systems
面试权重:★★★
来源:《深入理解 AI Infra:量化分析与系统设计》第 11 章「工具失败后的恢复」(write() ≠ 已持久化、持久性语义、Agent 事务 / ACID 方向)与第 9 章「部分故障与流式生成恢复」(token 级预写日志,DeepSeek V4 生成服务)。ACID、隔离级别、两阶段提交、Saga / 补偿、WAL 通用原理(DDIA 第 7 / 9 章、System Design 101、MIT 6.5840 等公开资料)未做一手考证 —— 这一部分两本参考书基本不覆盖,按渠道记录,不当作一手基线
相关知识点:../06-agent-infra/10-agent-runtime.md §6(分工:那篇是 Agent 工具执行层的失败恢复与持久性语义;本篇是通用机制层 —— 事务 / WAL / 补偿本身,与 Agent 无关也成立)、timeout-retry-idempotency.md(同批:超时后结果是「未知」,幂等是重试与补偿的共同前提)、state-machine.md(状态机与持久化执行,是 Saga 的落地形态)、queue-pubsub.md(投递语义:至少一次 / 恰好一次)、../04-agent-architecture/human-in-the-loop.md(中断前后副作用的幂等)、../../02-interview/design/08-agent-platform.md(控制面 / 执行面 / 状态存储三分)、../../02-interview/projects/quewi-chatbot.md

一句话#

事务要回答的是「一组改动怎么才叫一起发生」:单机靠 ACID + WAL 把「一条日志先落盘」换成「数据页随便什么时候刷」,跨服务则没有免费的午餐 —— 2PC 用阻塞换原子性,Saga 用补偿换可用性。落到 Agent 上,就是把「一轮 = 模型决策 + 工具执行 + 保存结果」当整体提交;而外部副作用恰好执行一次这件事,本地事务保证不了。

为什么重要(面试角度)#

  • 它是「一致性」这个词的落点。 泛泛说「保证一致性」没有信息量;能说清原子性 / 隔离性 / 持久性各自靠什么机制保证、而一致性是应用层的责任,层级立刻不同。
  • WAL 是理解一切存储系统的主线。 「先写日志再改数据」这一个决定,同时解释了崩溃恢复、复制、以及为什么顺序写不慢 —— 顺着它能把 redo / undo / checkpoint / 复制串起来。
  • 跨服务事务是设计题的高频分界。 说得出 2PC 的阻塞问题和补偿不总是可行的人,和只会说「用分布式事务」的人,是两个层次。
  • fsync 那条口径是书里的原话。 write() 返回 ≠ 已持久化 —— 这既是通用 OS 知识,也直接对应 Agent 运行时的「持久性语义」。
  • 和你的项目接得上:QueWI 网关没有事务场景(它只做模型调用,副作用是多花钱而不是改数据);要在面试里主动划这个界,不要硬把网关说成「分布式事务系统」。

⚠️ 边界声明:你没有在生产里设计或运维过事务系统 —— 没配过 2PC 协调者、没写过 Saga 编排、没调过隔离级别。本篇是通用机制的梳理,面试用「这套账我会这么算」的口径讲,不要说成「我们线上就是这么做的」。

核心内容#

1. ACID 逐条说人话#

性质 一句话 谁在保证
原子性 A 一组操作要么全做、要么全不做 数据库:靠 undo(回滚已做的部分)
一致性 C 事务前后,应用定义的不变式都成立(如「账户总额守恒」) 应用自己 —— 数据库不定义什么是一致
隔离性 I 并发事务互不干扰到什么程度 数据库:靠锁 / MVCC,程度由隔离级别决定
持久性 D 提交成功后,结果不因崩溃而丢 数据库:靠 redo + fsync

最容易被说错的是 C。 ACID 里的「一致性」不是数据库给的——数据库给的是 A / I / D。一致性是应用层的责任:事务写法本身有没有破坏业务约束(比如把余额扣成负数),数据库管不了。所以「用事务就保证一致性」是错的;事务保证的是「要么你写的都对,要么都没写」。

最容易被含糊带过的是 I。 它不是「有 / 没有」,而是一个光谱(第 2 节)。说「我们开了事务所以隔离住了」而不说级别,等于没说。

2. 隔离级别与对应的异常#

隔离越强,并发度越低 —— 这是一条单调的取舍,不是「越高越好」。

隔离级别 允许的异常 异常长什么样
读未提交 RU 脏读 读到了别的事务未提交的数据,对方一回滚,你读到的是从没存在过的值
读已提交 RC 不可重复读 / 读偏斜 同一事务里两次读同一行,结果不同(中间被别人改了);或同一快照内读到不一致的组合
可重复读 RR 幻读、写偏斜 同一范围的两次查询多出行来;或两个事务各自读到相同前提、各写不同行,两个不变式都被破坏
串行化 Serializable —— 无上述异常,代价是并发度(加锁等待 / 冲突重试)

几个必须点破的坑:

  • 「读未提交」现实中几乎没人用。 标准列了四级,但真实默认值基本只有两种:MySQL / InnoDB 默认 RR,PostgreSQL、Oracle 默认 RC。所以「你们跑在什么隔离级别」是个真问题,不是学术问题。
  • 标准 RR ≠ 各家的 RR。 SQL 标准里 RR 允许幻读;MySQL 的 RR 用「间隙锁 / next-key lock」额外规避了大部分幻读(这是实现层的行为,不是标准要求,且有边界情况)。而 PostgreSQL 的 RR 实际是快照隔离(snapshot isolation),它不幻读,但允许写偏斜。实现差异一律以各数据库当期文档为准,别把某家的行为当标准背。
  • 写偏斜是最容易被忽略的异常。 它不违反任何单行约束:两个医生同时看「还有两个医生值班」都去请假的经典例子里,每人各自扣掉自己那行,两条 UPDATE 都合法,合起来就没人值班了。它在快照隔离下就会发生 —— 提醒你「没幻读」不等于「可串行化」。
  • 串行化的代价要能说清:加锁等待与冲突重试会把吞吐压下来;要不要上串行化,取决于业务能不能容忍偶发的异常读,而不是「严格总更好」。

3. WAL:为什么先写日志再改数据#

数据库改一行数据,落盘时有两种做法:直接把数据页写回磁盘,或先把「我要改什么」写进日志、再慢慢刷数据页。所有主流存储系统选后者,原因只有一个:

顺序写(append-only)比随机写便宜得多。 日志是一条不断往后追加的流,写入是顺序的;数据页则散落在磁盘各处,按页随机写。把「随机写」换成「顺序追加 + 后台批量刷」,单次提交的延迟就降下来,吞吐上去。

崩溃恢复靠日志的两件事:

  • redo(重做) —— 事务已提交,但改动可能还在内存 / 只写到日志、没落到数据页;崩溃重启后按日志重放,把已提交的改动补上 → 保证持久性 D。
  • undo(回滚) —— 事务没提交就崩了,改动可能已经部分写进数据页;重启后按日志撤销这些改动 → 保证原子性 A。

同一份日志还顺手解决了复制:把 WAL 发给副本,副本重放,就是物理复制 —— 所以 WAL 不只是「崩溃恢复机制」,它是「把状态变更表述成一条有序流」这个思路的实例。这也解释了为什么很多系统(包括一些 Agent 记忆系统)会借鉴「只增日志 + 定期检查点」:日志保原始证据,检查点让恢复不用从头重放。

代价要写清:每提交一次都等日志落盘,吞吐会被 fsync 的延迟限制。所以有「组提交」把多个事务的日志一起刷(见第 4 节),也有把 synchronous_commit 调成异步的 —— 那是拿持久性换吞吐,要明说。

4. write() 返回 ≠ 已持久化#

这是《深入理解 AI Infra》第 11 章的原话口径(该书引 Linux write(2) / fsync(2) 手册页):write() 返回时,数据可能仍在内核的页缓存里 —— 程序可以继续算,由操作系统后台写盘。要确保数据已经存好,必须调用 fsync(),并检查它的返回值。 还有三个容易漏的细节:

  1. 检查返回值 —— fsync() 可能失败(磁盘满、I/O 错误),不检查就等于假设成功的。
  2. 目录也要 fsync() —— 新建或重命名文件时,文件内容持久化不等于文件名 / 目录项持久化,目录项可能需要单独对目录做 fsync。
  3. 文件 fsync 不提供多文件事务 —— 你把 A、B 两个文件都写好了,中间崩溃仍可能只留下一个。

由此带出「持久性语义」这个概念(../06-agent-infra/10-agent-runtime.md §6):「保存成功」其实有不同等级 —— 写入本地页缓存 / 完成本地 fsync() / 收到云端持久化确认,能扛的故障完全不同。本地 fsync() 能扛进程重启,扛不住整台机器加磁盘不可用。所以「我们保存了」这句话必须说清保存到哪一级。

5. 单机事务 → 跨服务:2PC 的阻塞问题#

单机的 ACID 靠数据库内部的日志和锁实现;一旦改动横跨两个服务(各自的数据库),就没有一个共同的日志和锁了。经典解法是两阶段提交(2PC):

  1. 准备阶段 —— 协调者让每个参与者先做好准备(执行但不提交、锁住资源),各参与者回报「可以提交 / 不行」;
  2. 提交阶段 —— 协调者看全体投票:全 yes 就发 commit,否则发 abort。

2PC 的致命问题是「阻塞」:参与者投票「可以」之后进入 prepared 状态,一直持有锁,等协调者的最终决定。如果协调者此时崩溃,参与者既不敢自己 commit(可能别人投了 no),也不敢自己 abort(可能别人都 commit 了)—— 它卡在中间,锁一直不放,这就是「in-doubt 事务」。协调者成了单点,而它的故障会直接冻结参与者持有的资源。

所以:微服务里很少用 2PC。它不是「过时」,而是「用可用性换原子性」的取舍 —— 长事务持锁、协调者单点、跨服务锁的延迟叠加,在多数业务里不划算。补充一点:有人会想到三阶段提交(3PC),它缓解了协调者崩溃的阻塞,但在网络分区下仍无法同时保证安全与可用 —— 这不是工程疏忽,是理论边界。

6. Saga / 补偿事务:用补偿换可用性#

Saga 的思路:不要一个横跨所有服务的大事务,而是拆成一条本地事务链 T1→T2→⋯→TnT_1 \to T_2 \to \dots \to T_n;每一步都是自己服务的本地事务(因此每一步都有本地 ACID)。如果第 kk 步失败,就逆序执行补偿操作 Ck−1→⋯→C1C_{k-1} \to \dots \to C_1,把前面已做的抵消掉。

  • 补偿不是回滚。 回滚是「当作没发生」,补偿是「再做一件事去抵消它」。区别是真实的:退款能让钱回来,但流水、时间戳、用户体验和「从没扣过款」不一样。
  • 补偿不总是可行 —— 这是设计阶段必须回答的问题。 发出去的邮件收不回、已推送的通知、已拨出的电话、已经对第三方生效的调用 —— 这些操作没有补偿。所以 Saga 不是「事后补救」的机制,它逼你在设计阶段就让副作用可补偿:
让副作用可补偿的手段 例子
延迟发送 / 两段式 先「预检 + 暂存」,等整链确认后再真正发出(「预检—确认」两段式见 ../../02-interview/design/08-agent-platform.md)
状态可回退 软删除 / 标记取消,而不是物理删除;版本化,允许指回旧版本
外部系统支持撤销 要求对方提供幂等接口 / 反向操作 / 操作标识(如支付网关的撤销接口)
把不可逆操作放到最后 让它成为流程里「一旦执行就不再有后续可失败步骤」的末尾
  • Saga 没有隔离性。 链跑的过程中,中间状态对外是可见的(钱已扣、货未发)。别的请求可能读到这个中间态。缓解靠语义锁(用一个「处理中」标记挡住对同一业务对象的并发操作)或让下游容忍中间态。说得出「Saga 换掉的不只是原子性的实现方式,还有隔离性」是加分项 —— 它本质上给的是最终一致,不是 ACID 事务。

7. 轻量替代:幂等键 + 去重表 + 状态机#

多数场景里,比 2PC 更实用的不是 Saga 本身,而是更小的一套组合:

  • 幂等键 + 去重表(见 timeout-retry-idempotency.md)—— 让重复调用安全,于是「重试」这件最普通的事就不需要事务兜底;
  • 状态机 + 持久化状态 —— 每个业务对象有显式的状态(待处理 / 处理中 / 已完成 / 已取消),重试时先读状态再决定:已完成就返回结果,处理中就等,未开始才执行。状态本身就是「要不要重做」的判据,见 state-machine.md;
  • 定时对账 / 补偿任务 —— 兜住漏网的中间态(每天扫一遍「扣了款没发货」的单子)。

为什么这套常常更划算:它不要求所有服务同时可用且长事务持锁,只要操作可标识、状态可查询。判据一句话:能用「幂等 + 状态」表达的,就别上 2PC。

8. 分布式共识:一句话带过#

Raft 用「多数派日志 + 状态机重放」解决一致性 —— 把每个操作作为日志条目复制给多数节点,各节点按同一顺序重放,就得到同一份状态。它和本篇的联系是:它也是一条 WAL,只是这条日志要「多数派同意」才算提交。细节(选举、日志匹配、成员变更)属于 MIT 6.5840 的内容,本篇不展开 —— 面试里点到「多数派日志 + 状态机重放」这个结构就够了,往深处问再另开。

9. 一个有出处的数字:token 级 WAL(书第 9 章)#

这是本篇唯一一个来自参考书的具体机制,值得记住,因为它把「WAL」从存储系统搬到了生成服务:

《深入理解 AI Infra》第 9 章 §9.6.3:DeepSeek V4 的生成服务采用预写日志 —— 在对外确认之前,可靠保存已经确定的输出序列。书里把两样东西分得很清:

  • token 级 WAL 保存已经确定的输出序列,决定恢复后应该继续哪条输出;
  • KV 保存的是这段序列已经完成的计算,决定还要重做多少工作。

书里还点出:恢复时要保持权重版本、token 位置索引和 decode 状态一致。出处:《深入理解 AI Infra》第 9 章「部分故障与流式生成恢复」(§9.6.3)。这不是本仓库的测量,是书里的机制描述。

10. 接回 Agent:把一轮做成一个事务#

《深入理解 AI Infra》第 11 章给的方向是:借用数据库事务的做法,把一轮「模型决策 — 工具执行 — 保存结果」作为一个整体提交 —— 这一轮的模型消息、工具结果、相关文件版本都保存成功后,才记「已提交」;重启后从最后一次完整提交接着跑。这正是 **Agent 事务(agent transaction)**研究的方向,即在 Agent 执行中实现数据库的 ACID。仓库里 ../06-agent-infra/10-agent-runtime.md §6 已经把这套账铺开(保存频率、异步上传的缺口、连续保存位置),本篇不重复。

但有一条硬边界必须一起说(书里原话):

如果一轮操作还调用了外部服务,仅在本地记下「已提交」,不能保证外部操作也恰好完成一次。 那一侧需要对方支持操作标识、幂等接口或补偿处理。

也就是说:本地事务能保证「我这边」的一致,管不到「对面」。这是一条信任边界,跨过去就只能靠协议(幂等 / 补偿),不能靠本地 fsync。

常见面试问法#

Q:ACID 分别靠什么实现?「一致性」是谁保证的? A:原子性靠 undo(回滚已做的部分)、持久性靠 redo + fsync(把已提交的补上)、隔离性靠锁 / MVCC(程度由隔离级别决定)。「一致性」是应用层的责任 —— 数据库不定义什么是一致,它保证的是「要么你写的都对,要么都没写」。所以「用了事务就保证一致性」是错的:事务写法本身破坏了业务不变式(比如把余额扣成负数),数据库管不了。

Q:隔离级别有哪几级?各自的异常是什么? A:读未提交允许脏读(读到别人未提交、可能回滚的数据);读已提交允许不可重复读 / 读偏斜;可重复读禁止上面两种,但仍允许幻读与写偏斜(MySQL 的 RR 用间隙锁额外规避了大部分幻读,实现层行为);串行化无这些异常,代价是并发度。要补两句:真实默认值基本只有两种(MySQL 默认 RR、PostgreSQL / Oracle 默认 RC);别把某家的行为当标准背,以当期文档为准。「没幻读」不等于「可串行化」(快照隔离下仍有写偏斜)。

Q:WAL 是什么?为什么先写日志再改数据? A:WAL = 先写日志、后改数据页。理由:顺序追加写比随机写数据页便宜得多,把「随机写」换成「顺序追加 + 后台批量刷」,提交延迟降下来。崩溃恢复靠两件事:redo 重放已提交但没落到数据页的改动 → 持久性;undo 撤销未提交事务的部分改动 → 原子性。顺带一提:把 WAL 发给副本就是物理复制 —— 所以 WAL 是「把状态变更表述成有序流」这个思路的实例。

Q:write() 成功了,数据就存好了吗? A:没有。 write() 返回时数据可能还在内核页缓存里(这是《深入理解 AI Infra》第 11 章引 Linux 手册的口径)。要确保持久化得调 fsync() 并检查返回值。三个容易漏的细节:fsync 可能失败要检查、新建 / 重命名文件时目录项要单独对目录 fsync、文件 fsync 不提供多文件事务。由此带出持久性语义:本地页缓存 / 本地 fsync / 云端持久化确认,能扛的故障完全不同。

Q:跨服务怎么保证原子性?为什么不用 2PC? A:2PC 的致命问题是阻塞。 参与者投票「可以」后进入 prepared 状态、一直持锁,等协调者的最终决定;协调者此时崩溃,参与者既不敢单方面 commit(别人可能投了 no)也不敢 abort(别人可能都 commit 了),卡在中间、锁不放(in-doubt 事务)。协调者是单点,它的故障会冻结参与者的资源。 所以微服务里少用 —— 不是过时,是用可用性换原子性。3PC 缓解了协调者崩溃的阻塞,但网络分区下仍无法同时保证安全与可用,这是理论边界不是工程疏忽。

Q:什么是 Saga?补偿和回滚有什么区别? A:Saga = 本地事务链 T1→⋯→TnT_1 \to \dots \to T_n + 逆向补偿链;第 kk 步失败就逆序执行 Ck−1→⋯→C1C_{k-1} \to \dots \to C_1。补偿不是回滚:回滚是「当作没发生」,补偿是「再做一件事抵消它」—— 退款能让钱回来,但流水和时间戳与「从没扣过款」不一样。关键点:补偿不总是可行(发出去的邮件收不回)。所以 Saga 逼你在设计阶段就让副作用可补偿(延迟发送 / 两段式、软删除可回退、要求外部系统提供撤销接口、把不可逆操作放最后)。还要主动说:Saga 没有隔离性,中间状态对外可见,需要语义锁,它给的是最终一致。

Q:那跨服务到底怎么做才对? A:多数场景用「幂等键 + 去重表 + 状态机」这套轻量组合,胜过去上 2PC。 幂等键让重试安全;状态机让每个业务对象有显式状态(待处理 / 处理中 / 已完成),重试时先读状态再决定(已完成返回结果、处理中等、未开始才执行);再配定时对账兜住漏网的中间态。判据一句话:能用「幂等 + 状态」表达的,就别上 2PC。

Q:把「一轮 Agent = 模型决策 + 工具执行 + 保存结果」做成事务,能保证什么、不能保证什么? A:能保证本地一致:这一轮的消息、工具结果、文件版本都保存成功才记「已提交」,重启后从最后一次完整提交接着跑 —— 这是 Agent 事务(ACID 方向)。不能保证外部副作用恰好执行一次 —— 书里原话:「仅在本地记下『已提交』,不能保证外部操作也恰好完成一次」。跨过信任边界那一侧,只能靠对方的操作标识 / 幂等接口 / 补偿处理。本地事务管得住「我这边」,管不住「对面」。

易错点#

易错说法 问题 应该说
「用事务就保证一致性」 把 C 当成数据库给的 数据库给的是 A / I / D;一致性是应用层的不变式,数据库不定义它
「隔离性有 / 没有」 它是光谱 隔离是级别问题;不说级别等于没说
「可重复读就不会有异常了」 漏了幻读 / 写偏斜 标准 RR 允许幻读;MySQL RR 用间隙锁部分规避;快照隔离下仍有写偏斜,「没幻读」≠「可串行化」
「串行化肯定最好」 忽略并发度代价 串行化用加锁等待 / 冲突重试换正确性;要不要上取决于业务能否容忍偶发异常读
「write() 成功就是存好了」 混淆写入与持久化 write() 返回时可能还在内核页缓存;要 fsync() 并检查返回值(书引 Linux 手册口径)
「对文件 fsync 就万无一失」 漏了目录与多文件 新建 / 重命名要对目录 fsync;文件 fsync 不提供多文件事务
「2PC 是老技术,淘汰了」 说法不准 它是用可用性换原子性的取舍,核心是协调者崩溃 → 参与者持锁阻塞(in-doubt)
「Saga 就是分布式回滚」 混淆补偿与回滚 补偿是「再做一件事抵消」,不是「当作没发生」;退款 ≠ 没扣过款
「失败了补偿一下就行」 补偿不总是可行 发出去的邮件收不回;要在设计阶段让副作用可补偿,不能事后补救
「Saga 也是事务,有 ACID」 漏了隔离性 Saga 没有隔离性,中间态对外可见;给的是最终一致
「本地记了已提交,外部操作就只执行一次」 越过信任边界 本地提交保证不了外部副作用恰好一次;要靠操作标识 / 幂等接口 / 补偿
把 QueWI 网关说成「分布式事务实践」 触碰仓库硬规矩 网关没有事务场景(副作用是多花钱,不是改数据);要主动划界

自测#

1. ACID 里的「一致性」到底谁保证?为什么说「用了事务就保证一致性」是错的?

答案

数据库保证的是原子性 A、隔离性 I、持久性 D,不是 C。 ACID 的「一致性」指的是应用定义的不变式在事务前后都成立(比如「转账前后两个账户总额守恒」)—— 不变式长什么样,是应用说了算,数据库不知道。

所以「用事务就保证一致性」是错的:如果事务写法本身破坏了业务约束(把余额扣成负数、把订单标成已完成却没扣库存),数据库照样提交 —— 它只保证「要么你写的都对,要么都没写」,保证不了「你写的是否合理」。

面试里更值钱的说法:把 C 拆出来,说清「A/I/D 是数据库的机制(undo / 锁 / MVCC / redo + fsync),C 是应用的契约」。

2. WAL 是什么?为什么「先写日志再改数据」比「直接改数据」好?redo 和 undo 各保证什么?

答案

WAL(预写日志)= 先把「要改什么」追加到日志、再慢慢把数据页刷盘。

好在顺序写:日志是不断往后追加的流,写入是顺序的;数据页散落在磁盘各处、按页随机写。 把「随机写」换成「顺序追加 + 后台批量刷」,单次提交延迟降下来、吞吐上去。

崩溃恢复靠两件事:

  • redo(重做) —— 事务已提交,但改动可能只在日志里、没落到数据页;重启后按日志重放补上 → 保证持久性 D;
  • undo(回滚) —— 事务没提交就崩,改动可能已部分写进数据页;重启后按日志撤销 → 保证原子性 A。

顺带一提:把 WAL 发给副本、副本重放,就是物理复制 —— 所以 WAL 是「把状态变更表述成一条有序流」这个思路的实例,这也是为什么很多系统会借鉴「只增日志 + 定期检查点」。

3. 一个参与者说「我准备好了」,然后协调者崩了。会发生什么?为什么这就让微服务不愿用 2PC?

答案

参与者进入 prepared 状态、持有锁,卡在「等最终决定」上。 它不敢自己 commit(别的参与者可能投了 no,自己提交就会不一致),也不敢自己 abort(别人可能都 commit 了)—— 这就是 in-doubt 事务,锁一直不放,占着的资源无法释放。

所以 2PC 的致命问题是「阻塞」,而不是「慢」:协调者是这个协议的单一决策点,它一崩,参与者持有的资源就被冻结。

微服务里少用的原因:长事务持锁、协调者单点、跨服务锁的延迟叠加 —— 对多数业务来说用可用性换原子性不划算。补一句:3PC 缓解了协调者崩溃的阻塞,但在网络分区下仍无法同时保证安全与可用 —— 这是理论边界(CAP 那类结论),不是工程疏忽。

4. 什么是 Saga?补偿和回滚有什么本质区别?为什么说「设计阶段就要让副作用可补偿」?

答案

Saga = 本地事务链 T1→⋯→TnT_1 \to \dots \to T_n,第 kk 步失败就逆序执行补偿 Ck−1→⋯→C1C_{k-1} \to \dots \to C_1。 每一步都是自己服务的本地事务(因此有本地 ACID)。

补偿 ≠ 回滚:回滚是「当作没发生」,补偿是「再做一件事去抵消它」。真实差别:退款能让钱回来,但流水、时间戳、用户看到的东西,和「从没扣过款」不一样。

为什么要在设计阶段就让副作用可补偿:因为补偿不总是可行 —— 发出去的邮件收不回、已推送的通知、已对第三方生效的调用都没有补偿。所以手段必须前置:

  • 延迟发送 / 两段式 —— 先预检暂存,整链确认后再真正发出;
  • 状态可回退 —— 软删除 / 版本化,而不是物理删除;
  • 要求外部系统支持撤销 —— 幂等接口 / 反向操作 / 操作标识;
  • 把不可逆操作放到流程最后 —— 一旦执行就不再有任何后续可失败的步骤。

必须主动补充的一条:Saga 没有隔离性 —— 链跑的过程中「钱已扣、货未发」这样的中间状态对外可见,别的请求可能读到;缓解靠语义锁。它给的是最终一致,不是 ACID 事务。

5. 把「一轮 Agent = 模型决策 + 工具执行 + 保存结果」做成事务,能保证什么?不能保证什么?

答案

能保证的是「本地一致」。 借数据库事务的做法:这一轮的模型消息、工具结果、相关文件版本都保存成功,才记「已提交」;重启后从最后一次完整提交接着跑。这就是《深入理解 AI Infra》第 11 章说的 Agent 事务(ACID 方向),具体账(保存频率、异步上传的缺口、连续保存位置)见 ../06-agent-infra/10-agent-runtime.md §6。

不能保证的是「外部副作用恰好执行一次」。 书里的硬边界原话:「仅在本地记下『已提交』,不能保证外部操作也恰好完成一次。」 如果这一轮调了外部服务(付款、发消息、创建资源),本地事务管得住「我这边」,管不住「对面」。

跨过这条信任边界只能靠协议:目标系统的操作标识、幂等接口或补偿处理。所以真实系统里,「一轮 = 事务」和「外部调用带幂等键」是配套使用的两件事,缺一个就会在崩溃恢复时要么漏、要么重。