跳转到内容

D4 · 持久执行、并发与未知结果

本日安排:8 小时净学习。休息另计,算法可独立安排。

  • 2.5 小时:AG19–AG21、AG29–AG30,恢复与审批。
  • 1.5 小时:NC05–NC06,并发与取消。
  • 2 小时:DS01–DS03,幂等、Outbox 与 MQ。
  • 1 小时:退款超时故障演练。
  • 1 小时:口述与纠错。

想象一笔退款:本地操作状态写成 PROCESSING,worker 把请求发给支付通道;通道完成退款,但响应在网络中丢失。worker 在保存成功结果前崩溃。恢复后,本地只知道请求曾开始,不知道对方是否完成。把 Agent 的 checkpoint 恢复到调用前,再跑一遍工具,可能造成第二次退款。把状态恢复到调用后也不行,因为本地并没有可靠证据说明调用结果。

Checkpoint 保存的是工作流实现选定的状态边界。LangGraph 的 checkpoint 通常围绕图的 super-step 和节点写入持久化;中断后恢复可能从节点开头重新运行,节点中断前的普通代码也会再次执行。因此,不可重复的写操作不能靠“这段节点已经跑过”这种易失内存判断来防重。节点逻辑应能安全重入,或把外部操作拆成带稳定业务身份的步骤,结果由业务存储记录。1

这也解释了为什么 InMemorySaver 不能当作进程重启后的持久化:数据只在内存里,进程退出后就没有了。生产环境要选能持久保存状态的实现,并考虑保留、加密、结构版本升级、并发读写和故障恢复。即便 checkpoint 确实落盘,它仍只描述 Agent 执行状态,不会自动和支付系统的提交组成一个原子事务。

Temporal 的工作流通过历史重放恢复控制流,要求 Workflow 代码符合确定性约束;网络调用、数据库访问和模型请求等外部交互放在 Activity。历史可以重放已完成 Activity 的结果,但 Activity 在 worker 失败、超时等情形仍可能重新执行,所以外部写需要幂等设计。换框架改变的是持久执行的机制,不会消除下游副作用的不确定性。3

三种身份要分开:run、invocation、业务 operation

Section titled “三种身份要分开:run、invocation、业务 operation”

run_id 标识一条 Agent 工作流,便于恢复、查询状态和追踪事件。invocation_id 标识某次模型或工具请求尝试,适合日志、延迟和错误归因。operation_id 标识用户授权的业务意图,例如“订单 P-17 退款 1,200 日元,本例以日元为记账单位”。同一个 operation 可以经历多次 invocation、worker 重启和网络重试;这些尝试不能因此变成新的退款。

工程上可以在 refund_operations 表中保存 operation_id、业务幂等键、规范化请求摘要、状态、下游幂等键和版本号。唯一约束保护一个业务身份只对应一个操作。收到相同幂等键、相同请求摘要时,服务返回现有 operation 的状态或结果;键相同但对象、金额、币种或动作不同,则拒绝请求,不能静默改写原意图。参数摘要负责发现“同键异参”,不应拿它取代业务意图身份:用户可能有两笔金额恰好相同但完全独立的合法退款。

服务端先校验调用主体和参数,再在本地事务中创建 operation、预留资源并写入待处理事件。唯一约束竞争时,一方提交,另一方回滚后读取已存在记录并比较请求摘要。不能用“先查没有、再插入”代替唯一约束,因为两个并发请求都可能在查询时看到空结果。幂等记录也不能随意按短 TTL 删除;下游保留期、客户端重试窗口和对账窗口都要纳入生命周期。以 Stripe 为例,接口文档明确说明幂等键的参数匹配和清理期限;该期限是特定服务的契约,不是所有支付接口的通用保证。4

外部请求超时要进入 UNKNOWN 或 RECONCILING,不能直接写成 FAILED。超时只说明本服务没按时得到答复,不证明远端没有执行。未知期间维持额度预留,按原下游键查询或重试(前提是服务商契约仍覆盖该键);也可以等待回调或人工对账。只有确认下游不会再执行,才能把 operation 标为最终失败并释放预留。下游幂等过期、查询也无法确认时,不生成新键碰运气。

Outbox 与 Inbox:处理本地双写,不跨越支付边界

Section titled “Outbox 与 Inbox:处理本地双写,不跨越支付边界”

本地数据库事务与消息队列发布是两个独立提交。如果先提交数据库、发布消息前进程退出,数据已变更但 worker 不会被唤醒;如果先发消息、数据库事务再回滚,消费者就看到了不存在的业务变化。Transactional Outbox 把业务数据和待发布事件写入同一个数据库事务。relay 之后扫描或订阅 outbox,再把事件送进队列。AWS 的模式说明也特别指出,relay 已发布但来不及标记时可能发生重复投递,因此消费端仍要幂等。5

一种常见的消费者方案是 Inbox:消费者以 event_id 为唯一键,在同一个本地事务里写消费记录并更新本地业务状态。重复事件撞上唯一约束后,不重复应用本地更新。若消费者处理完 Inbox 后还要调用退款 API,边界又向外延伸了一层:Inbox 只能证明本地事件被登记,不能证明退款恰好完成。应把下游调用建成独立 operation,并继续使用稳定下游键、查询状态和对账。

事件应带 event_id、聚合对象 ID、版本号和事件类型。消费者需检查状态转移是否合法:重复的 refund.succeeded 不重复加已退款金额;迟到的 refund.processing 不能覆盖已确认成功的状态;版本缺口则补查权威 operation,而不是把收到顺序当成业务事实。消息队列的“恰好一次”描述通常有明确的系统内边界,不能扩写成跨数据库、支付网关和任意回调均只产生一次业务效果。

Node.js 的 JavaScript 回调默认由事件循环线程执行,I/O 等待期间事件循环可以处理其他回调。这里的“单线程”不等于“一次只有一个业务操作在进行”:多个请求在等待数据库或网络时并发推进。Node 文档描述的事件循环负责运行 JavaScript 回调,并协调非阻塞 I/O;耗时同步计算仍会占住事件循环,影响其他请求。6

例如,可退款额度为 100。请求 A 读到剩余 100 后 await 等待计算;请求 B 同样读到 100,预留 80 并提交;A 恢复后也按旧读数预留 80,累计预留 160。即使每段 JavaScript 按顺序执行,整个“读取—等待—写入”业务过程仍发生交错。解决方法是在权威数据库中用条件更新或行锁检查并预留,例如 UPDATE ... WHERE cap - refunded - reserved >= :amount,并确认只更新一行。进程内 Mutex 只约束本进程,无法覆盖多实例与其它写入者。

工具也要分清用途:Mutex 让同一临界区一次只运行一个任务;Semaphore 限制同时运行的任务数;CAS 或版本号更新在读到的版本仍匹配时才写入;背压限制上游生产速度,避免无限队列。它们不能相互替代:Semaphore 保护支付通道并发,不保证每笔支付的额度正确;数据库条件写入保护额度,不控制队列是否持续堆积。

取消是协作信号,不是事务回滚。Node 的 AbortSignal 可通知支持取消的请求停止等待或停止执行,但请求可能已经到达远端,远端写也可能已经提交。调用方应传播 deadline 和取消信号,停止领取新工作,并在 finally 释放本地资源。对已经发出的退款仍要查询状态;UI 上的“已取消”不能抹掉外部事实。

恢复模型如何选:按执行时长与副作用边界

Section titled “恢复模型如何选:按执行时长与副作用边界”

短任务、少量工具、无需跨进程恢复时,直接 API 调用加明确循环最容易审查。Agent SDK 可以封装工具调用、handoff 和 tracing;图框架便于表达条件路由、中断和可检查状态;持久工作流引擎适合长等待、服务重启恢复和活动重试。选择标准包括任务会持续多久、是否等待审批、状态由谁保存、失败后哪一层负责重试、团队愿意维护什么运行时。

可以组合工具,但必须确定唯一的恢复权威:谁拥有 run_id,谁是业务 operation 的状态来源,模型调用与工具调用各自由哪一层重试。若 SDK、图框架和工作流引擎各自重试同一动作,三层各试三次,最坏会形成 27 次下游调用。用统一预算、退避和截止时间;明确非重试错误、并行取消和未知结果分支。任何框架的 checkpoint 都不能替代业务状态表与支付方对账。

工作流恢复边界与外部操作身份关系

上图中的箭头表示控制与数据先后,不代表真实时间间隔。checkpoint 恢复状态后,业务 operation 仍需凭自己的稳定身份查询或重试。

Outbox、Inbox 与外部副作用边界

生产端的本地事务同时写业务状态与 Outbox;消费端在自己的本地事务中登记 Inbox 并更新本地状态。队列投递和支付调用不在这些事务中,消息可以重复。

支付调用已发出,客户端等待超时且无回调。服务应如何记录?

  • A. 写成 FAILED 并释放预留,稍后可新建请求
  • B. 写成 UNKNOWN,保留预留,按原 operation 身份查询或按契约同键重试
  • C. 生成新的下游幂等键,避免旧键缓存错误
  • D. 回滚本地事务,视为支付方也回滚
答案与解析

正确答案:B。A 会把“不知道”误作“确定未执行”,可能超额退款;C 把同一意图变成新请求;D 的本地回滚不撤销已提交的远端操作。

恢复一个包含外部写操作的节点时,哪项设计最可靠?

  • A. 假设节点成功执行过就不会再执行
  • B. 只要 checkpointer 落盘,远端也一定只执行一次
  • C. 用稳定业务 operation ID,持久化状态,并使副作用可查询或按同键幂等
  • D. 重新调用模型生成一组新参数
答案与解析

正确答案:C。A 忽略恢复和重试;B 把工作流持久化误当分布式事务;D 会改变请求内容或身份,不能替代核对事实。

Outbox relay 发布消息后,在更新 published_at 前崩溃。消费者应预期什么?

  • A. 消息绝不会重复,因为数据库事务已提交
  • B. 消息可能重复,消费者按 event ID 幂等应用本地效果
  • C. 整个数据库事务会自动回滚
  • D. 只需让队列按时间戳排序
答案与解析

正确答案:B。A 忽略数据库与队列是不同提交;C 不会跨系统回滚;D 保序也不能消除重复或验证业务状态转移。

Node 服务用 await 调用数据库之前读取库存,为什么仍可能超卖?

  • A. await 会自动让所有请求同时写入
  • B. 单线程仍可在等待期间处理其它请求,后续按过期读数写入
  • C. Promise 会在数据库端开启分布式事务
  • D. 只要改用 Semaphore,数据库写入自然原子
答案与解析

正确答案:B。A 不准确,交错发生在等待边界;C 不成立;D 限制并发数但不能替代库存条件更新或事务约束。

多个服务实例共同领取任务,限制每实例并发 10 个能否保证全局最多 10 个?

  • A. 能,Semaphore 天然跨进程共享
  • B. 能,只要请求运行在 Node.js
  • C. 不能,全局并发可能接近实例数乘 10,需共享预算或集中限额
  • D. 不能,Semaphore 不适合限制并发
答案与解析

正确答案:C。A、B 把进程本地机制误作分布式协调;D 错,Semaphore 正适合限制某作用域内在途任务,但其作用范围必须说明。

以下哪句话最准确地描述 checkpoint 与退款之间的关系?

  • A. 保存 checkpoint 等于支付成功结果已原子提交
  • B. Checkpoint 能恢复工作流状态;退款是否发生要由支付方状态和业务记录确认
  • C. 只要使用 Temporal,Activity 就永不重跑
  • D. 只要收到取消信号,已提交退款会自动冲销
答案与解析

正确答案:B。A 混淆两个系统;C 忽略 Activity 重试;D 把协作取消误作业务补偿。

操作为 PROCESSING,支付通道已成功但响应丢失,worker 在写本地成功状态前退出。请说明恢复时本地和外部各自掌握什么,以及安全的下一步。

参考答案与评分点

本地只能证明 operation 已开始,不能从超时推断支付失败;支付方可能已完成。使用原 operation ID 和下游幂等键,查状态或接收经验证的回调;确认成功后在本地事务中更新状态与额度。结果仍未知时保留预留并对账或转人工,不创建新键。

评分:1 分区分本地事实与远端事实;1 分复用稳定身份;1 分给出查询/回调/同键重试之一;1 分未知时不释放预留、不换键。

解释 operation ID、invocation ID 与幂等键分别用于什么,并说明为什么一次重试不应生成新的业务 operation。

参考答案与评分点

Operation ID 识别一次业务意图;invocation ID 识别某次工具或模型调用尝试;幂等键让服务端把同一意图的重试关联到既有结果。重试只是同一操作的执行尝试,不代表用户提出新意图;应校验同键参数摘要一致。新业务意图才创建新 operation 和新键。

评分:1 分准确说明三种身份中的两种;1 分说明幂等键功能;1 分指出重试不等于新意图;1 分指出同键异参要拒绝或校验。

Outbox 与 Inbox 各自解决什么问题?消费者还要调用支付 API 时,为什么不能把 Inbox 说成 exactly-once 保障?

参考答案与评分点

Outbox 把业务更新和待发布事件放在同一个本地事务,避免双写空洞。Inbox 让消费者在本地事务中记录 event ID 并应用本地业务效果,重复消息可被识别。支付 API 是新的外部提交边界,Inbox 的本地提交不能与支付原子化,因此仍需 operation 身份、下游幂等、查状态和对账。

评分:1 分说明 Outbox;1 分说明 Inbox 的本地原子去重;1 分指出支付属于另一个边界;1 分给出适当的下游恢复办法。

一个 Node 服务在多副本部署,既要保护支付通道并发,又要防止单笔支付超额退款。应如何分别处理?取消请求有什么限制?

参考答案与评分点

通道并发用有作用域说明的 Semaphore、共享限额或队列消费配额;每笔支付的金额不变量放在权威数据库,用条件更新、版本或事务原子预留。进程内锁不够覆盖多个副本。取消信号停止尚未开始的工作并通知可取消调用;已发出的支付请求仍需查询或对账,不能由取消自动撤销。

评分:1 分区分资源并发和业务一致性;1 分使用共享/全局限额概念;1 分使用权威数据库原子约束;1 分说明取消不能撤销既成外部效果。

下面保留原报告的题目和口述参考答案。先自行作答,再展开查看。

AG19|Checkpoint 保存后,Agent 能从断点精确继续吗?〔P0〕

口述答案

要先说明断点的粒度。Checkpoint 通常保存图或工作流的某个状态边界,不是保存整个进程的任意机器指令位置。LangGraph 的持久化按图执行边界记录状态;中断恢复可能重新进入节点,所以节点前半段的代码和外部副作用可能再执行。内存里的 checkpointer 也不抗进程退出。

生产上把不可逆操作做成有稳定业务身份的独立步骤;保存调用意图、已知结果和待核查状态。模型调用结果可按系统契约持久化复用,但“工作流状态已经保存”与“外部退款只发生一次”是两个不同保证。

追问怎么接

“Temporal 呢?”工作流按历史重放,要求工作流代码确定性;网络调用、模型调用等放到 Activity 一类的外部执行单元。外部调用可能重试,仍要幂等。“节点成功但 checkpoint 前崩溃呢?”可能重复执行,所以不能把成功返回的内存标记当成事务提交。

不要这样答:“checkpoint 就是 exactly-once”“恢复时从 interrupt 后面那一行直接接着执行”。

依据与延伸:LG-Checkpoint、LG-Interrupt、Temporal-Workflow、Temporal-Activity。

AG20|工具超时,但退款可能已完成,怎么办?〔P0〕

口述答案

将结果标为 UNKNOWN,而不是直接标失败。应用用稳定的业务幂等键和已保存的请求内容,查询支付方状态;只有在契约允许时才以相同键重试。收到回调后也要验签、去重并核对请求目标。如果下游既不支持幂等也无法查询,那么不能凭本地状态保证安全自动重试,需对账或人工处理。

必须区分“没发出去”“确定拒绝”“已知成功”“发出后结果未知”。在未知时提前释放可退款额度,可能让另一个操作再退款,造成累计超额。

追问怎么接

“有数据库事务不能解决吗?”本地事务覆盖不了独立支付系统的提交;一直持有数据库事务等网络也不解决不确定性。“换一个模型重新调用?”会制造新请求身份;先恢复原业务操作的事实,不能用重新推理代替对账。

不要这样答:“超时重试三次就行”“捕获异常后回滚本地事务,就等于远端退款撤销了”。

依据与延伸:AWS-Retry、Stripe-Idem、Stripe-Webhook。

AG21|人工审批怎么做,才能真的约束执行?〔P0〕

口述答案

审批对象应该是不可变或有版本的具体提案:商家、订单/申请、动作、金额、币种、关键状态版本、有效期和请求者,而不是一段可能继续变化的聊天文本。批准后保存批准人、时间、权限和提案摘要/哈希;执行时重新校验身份、最新业务约束与提案一致性。

审批与执行之间状态可能变化,因此需要原子地校验并预留资源,或用版本条件更新。人工批准不是永久授权,金额或目标变化应重新审批。暂停期间释放工作线程/数据库连接,把等待状态持久化。

追问怎么接

“批了以后 Agent 可以修改参数吗?”不能悄悄修改;执行参数应来自被批准提案而非再次自由生成。“审批链接被重复点两次?”批准操作与执行操作各自幂等;从状态机与唯一约束阻止重复推进。

不要这样答:“用户回复 yes 就给下一次任意 refund 调用放行”,或持有数据库锁等人回复。

依据与延伸:Approval、LG-Interrupt、DB-LockingRead。

AG29|如何把一个 Agent 循环做成可扩展后端服务?〔P0〕

口述答案

把请求接入与长任务执行分离:创建 run 后返回标识,由 worker 从持久任务队列或数据库领取工作;状态、事件和工具操作结果保存到持久存储,客户端用查询或事件流跟踪。为运行设置租户配额、并发限额、deadline 和取消状态。

并发 worker 不能仅凭“读到状态是 RUNNING”都执行,需要带版本的领取/续租/完成协议。过期 worker 恢复后不得覆盖新状态;租约能协调执行,但不能单独阻止它已发到外部的请求,外部写仍靠稳定业务身份与下游幂等。

追问怎么接

“浏览器断开,任务结束吗?”由产品契约决定;持久任务通常继续,断开只影响订阅。“取消是不是立即终止?”停止派发并传播取消;进行中的副作用可能完成,返回部分完成和待核查状态,而不是抹除已完成结果。

**不要这样答:**每个用户请求开一个永不落盘的 while 循环,并认为负载均衡能负责恢复。

依据与延伸:LG-Checkpoint、Temporal-Activity、Node-Abort、Outbox。

AG30|手写循环、Agent SDK、LangGraph、Temporal 怎么选?〔P0〕

口述答案

少量工具、短任务时,直接 API 加清晰循环是合理基线;SDK 提供工具、handoff、会话和 tracing 等封装;LangGraph 适合显式图状态、中断、checkpoint 与可检查的执行流程;Temporal 更侧重持久工作流、重试、长时间等待与外部活动。它们有重叠,也可以分层组合。

选择前问:任务多长,是否跨进程恢复,是否人工等待,副作用如何幂等,团队愿意承担多少基础设施。即使用图或工作流引擎,业务数据库与支付状态也不由框架自动保证。

追问怎么接

“都组合起来最完整?”很容易出现三套重试、三套状态与重复恢复。真要分层时,明确外层唯一恢复权威:谁 owns run、谁重试模型/工具、谁推进审批;下层只执行受控步骤。“面试是不是必须会 LangChain?”执行契约比记 API 更重要。

不要这样答:“Temporal 和 LangGraph 完全等价”,或“框架越多,系统就越生产级”。

依据与延伸:SDK-Run、LG-Overview、Temporal-Workflow、Temporal-Activity。

NC05|Node.js 单线程,为什么还有并发和竞态?〔P0〕

口述答案

普通 JS 回调在一个 event loop 线程上执行,但 I/O 等待期间可让别的请求继续,所以有并发;这不等于 CPU 密集 JS 自动在多个核上并行。线程池与 worker_threads 承担的工作要分别看,CPU 密集任务可转移到 worker 或独立执行服务。

单线程也有业务竞态:请求 A 读余额后 await,请求 B 在此期间修改余额,A 恢复后按旧值写回,就会丢更新。Promise.all 只是聚合多个异步操作,失败时不会自动取消其他请求,更不会提供事务原子性。

追问怎么接

“Promise.resolve().then 能把大计算放后台吗?”不能,计算仍会占住执行 JS 的线程。“nextTick、Promise、timer 的顺序呢?”先明确 CommonJS/ESM、回调上下文与版本;理解 nextTick/microtasks 与事件阶段即可,不盲背一个跨环境序列。“解决余额竞态?”在业务数据库做原子条件更新。

不要这样答:“Node 单线程所以线程安全,也就没有业务并发问题”“async 函数里的循环不会阻塞”。

依据与延伸:Node-Loop、Node-Blocking、Node-Workers、JS-Promise。

NC06|Mutex、Semaphore、CAS、背压与取消怎么区分?〔P0〕

口述答案

Mutex 保护互斥临界区;Semaphore 限制同时进入的任务数;CAS 在值符合预期时原子更新,可用于乐观并发控制;背压是让上游发起速度服从下游处理能力,而不是无限积压。它们作用范围不同:进程内锁不能保护另一个服务实例,数据库版本条件可以约束多个实例竞争的业务状态。

取消通常是协作式信号。调用方发出 AbortSignal/context cancellation,被调用逻辑必须观察并响应;它不自动撤销已经提交的写操作。释放资源用 finally 等可靠路径,关闭 worker 时先停止领取,再收尾或记录待恢复任务。

追问怎么接

“开一个 semaphore 就能保护整个集群配额?”每实例各有一个上限时,总并发仍乘实例数;需要全局或分层预算。“死锁有哪些条件?”互斥、占有且等待、不可抢占、循环等待;工程上用统一锁顺序、短临界区与可取消等待减少风险。共享内存读写还要遵守语言内存模型,不只保证单条指令原子。

不要这样答:“取消 HTTP 请求就撤销远端事务”“进程内互斥锁能保护所有副本”。

依据与延伸:Go-Sync、Go-Semaphore、Go-Memory、Node-Abort、Go-Context、DB-Deadlock。

DS01|一个创建/退款接口如何实现幂等?〔P0〕

口述答案

先定义“同一次业务意图”的身份,客户端或服务端生成稳定 key,按租户/调用主体划定作用域。服务端保存规范化请求内容的哈希、业务 operation_id 与状态,使用唯一约束处理并发;相同 key、相同参数返回原操作结果或当前进度,相同 key、不同参数应拒绝。

幂等记录必须与本地业务写处于同一事务边界,或本身就是唯一业务操作记录,不能先写一个短 TTL Redis 标记就认为安全。跨支付系统时,还要复用稳定下游键并按其契约处理未知结果和键保留期限。

追问怎么接

“为什么不直接用参数哈希作 key?”两次合法、内容相同的独立业务意图可能不应合并;身份应来自业务意图,参数哈希用来防 key 复用冲突。“幂等记录过期呢?”要与最长重试/对账窗口和业务唯一约束匹配,不能让过期自动允许重复退款。

不要这样答:“先 SELECT 没有记录再 INSERT,所以天然幂等”“随机生成新的 retry key”。

依据与延伸:Stripe-Idem、DB-LockingRead、Outbox。

DS02|数据库已提交,但消息没有发出去,怎么办?〔P0〕

口述答案

使用 Transactional Outbox:在同一本地事务里写业务数据与 outbox 事件,提交后由 relay 通过轮询或 CDC 投递消息。这样不会出现业务已提交却没有任何可恢复事件记录的双写空洞。投递成功但 relay 来不及标记就崩溃,仍可能重复投递。

因此消费者也要幂等。若消费者只改本地数据库,可在同一事务中写 inbox/消费记录与业务效果,用唯一事件 ID 去重;如果还调用外部 API,就再面对外部副作用边界,不能只提前记“消费过了”。

追问怎么接

“为什么不先发 MQ,再提交 DB?”消息可能被消费而 DB 最终回滚。“Outbox 能保证消息恰好一次到达?”不能,通常按可重试投递设计。“发出以后永久重试不怕吗?”要区分可恢复/毒消息,监控积压与重试次数,支持人工处理和重放。

不要这样答:“数据库事务包住 MQ send 就成了分布式事务”“Outbox 不需要消费者去重”。

依据与延伸:Outbox、Kafka。

DS03|MQ 保序和 exactly-once 能保证业务正确吗?〔P0〕

口述答案

必须说清范围。Kafka 的顺序通常以分区为单位;其事务处理能力可以协调 Kafka 内部的输出与 offset,但任意外部支付/数据库副作用不会自动获得同样保证。跨系统仍需正确的提交边界、去重、幂等或对账。

业务处理不要只依赖到达顺序:事件带 event_id、aggregate_id 和版本号,按订单/申请的状态机允许合法转移。处理重复不重复生效,处理旧事件不把状态倒退;发现版本缺口时按业务选择补查权威状态或等待,不能简单丢弃所有晚到消息。

追问怎么接

“所有订单用一个分区就全局有序?”会限制吞吐,而且不自动约束绕开队列的写入。“付款成功回调比处理中回调先到?”用版本与真实支付状态处理;状态机要定义冲突解决,而不是覆盖最后收到的值。

不要这样答:“用了 exactly-once MQ,所以绝不会重复扣款”“消息有序所以不需要状态校验”。

依据与延伸:Kafka、Stripe-Webhook。