2025年3月20日·阅读约1分钟

事件驱动工作流 与 请求-响应 API:长期任务对比

比较事件驱动工作流与请求-响应 API 在长时运行流程中的应用,重点讨论审批、定时器、重试和审计跟踪在业务应用中的处理。

事件驱动工作流 与 请求-响应 API:长期任务对比

为什么长期运行的流程在业务应用中棘手

当一个流程不能在一次快速步骤中完成时,就称为“长期运行”。它可能需要几分钟、几小时或几天,因为它依赖于人工、时间或外部系统。任何包含审批、交接和等待的场景都属于这类。

这时,简单的请求-响应 API 思维开始失效。API 调用适合短交互:发送请求,得到回答,然后继续。长期任务更像是有若干章的故事。你需要暂停,精确记住进度,并在以后继续而不靠猜测。

你会在日常业务应用中见到这些问题:需要经理和财务审批的采购、等待文件核验的员工入职、依赖支付提供商的退款,或必须先审核再授予的访问请求。

当团队把一个长期流程当作单次 API 调用来处理时,会出现一些常见问题:

  • 应用在重启或部署后丢失状态,无法可靠恢复。
  • 重试产生重复:第二次付款、第二封邮件、重复的审批。
  • 责任不清:没人知道接下来应该由申请人、经理还是系统任务来处理。
  • 支持无法可视化流程卡在哪儿,不得不翻日志来排查。
  • 等待逻辑(定时、提醒、截止)被拆成脆弱的后台脚本。

举个具体场景:员工请求软件访问。经理很快批准,但 IT 需要两天来配置。如果应用不能保存流程状态、发送提醒并安全恢复,就会产生人工跟进、用户困惑和额外工作。

这就是在长期运行业务流程中,选择事件驱动工作流还是请求-响应 API 的重要性所在。

两种思维模型:同步调用 vs 随时间发生的事件

最简单的对比归结为一个问题:工作是在用户等待时完成,还是在他们离开后继续进行?

请求-响应 API 是一次交换:一次调用进来,一次响应出去。它适合快速且可预测完成的工作,比如创建记录、计算报价或检查库存。服务器完成工作,返回成功或失败,交互结束。

事件驱动工作流是一系列随时间发生的反应。某件事发生了(订单被创建、经理批准、定时器触发),工作流就进入下一步。该模型适合包含交接、等待、重试和提醒的工作。

更实用的区别在于状态。

在请求-响应模型中,状态通常存在于当前请求和服务器内存中,直到响应发送完毕。而在事件驱动工作流中,必须把状态保存下来(例如在 PostgreSQL 中),以便流程以后能继续。

故障处理也会不同。请求-响应通常通过返回错误让客户端重试来处理失败。工作流会记录失败并在条件改善时安全重试。它们也能把每一步记录为事件,从而更容易重建历史。

一个简单的例子:"提交报销单" 可以是同步的。"获取审批,等待 3 天,提醒经理,然后付款" 就不能是同步的。

审批:两种方法如何处理人工决策

审批是长期工作变得真实的地方。系统步骤可能在毫秒内完成,但人可能在两分钟或两天内答复。关键设计选择是你是否把等待建模为一个暂停的流程,还是把它看作稍后到达的一条新消息。

在请求-响应 API 中,审批常常成为一种尴尬的形态:

  • 阻塞(不可行)
  • 轮询(客户端不停问“批准了吗?”)
  • 回调/Webhook(服务器稍后回调你)

这些方法都能工作,但它们只是增加了桥接“人工时间”和“API 时间”的额外配线。

在事件驱动模型中,审批更像是讲故事。应用记录类似 “ExpenseSubmitted” 的事件,然后稍后接收到 “ExpenseApproved” 或 “ExpenseRejected”。工作流引擎(或你自己的状态机)仅在下一个事件到达时推进记录。这符合大多数人对业务步骤的直觉:提交、审查、决策。

复杂性会很快出现,特别是当有多位审批人和升级规则时。你可能需要经理和财务都批准,同时又允许高级经理覆盖决定。如果不把这些规则清晰建模,流程会变得难以推理,更难审计。

一个可扩展的简单审批模型

一个实用的模式是保留一个“请求”记录,然后把决策单独存储。这样可以在不重写核心逻辑的情况下支持多位审批人。

把几类数据作为一等公民记录下来:

  • 审批请求本身:被审批的内容及其当前状态
  • 单个决策:谁决定、同意/拒绝、时间戳、原因
  • 必需审批人:角色或具体人员,以及任何排序规则
  • 结果规则:“任意一人通过”、“多数通过”、“所有必需通过”、“允许覆盖”

无论采用何种实现,总是把谁在何时以何理由批准了什么作为数据保存,而不是仅仅作为一行日志。

定时与等待:提醒、截止与升级

等待是长期任务开始显得混乱的地方。人去吃饭,日程排满,“我们会回复你”变成了“现在谁负责?”这是事件驱动工作流与请求-响应 API 最大的差别之一。

在请求-响应模型中,时间处理很尴尬。HTTP 调用有超时限制,你不能把请求打开两天。团队通常采用轮询、单独的定时任务扫描数据库,或在逾期时运行手动脚本。这些方法能工作,但等待逻辑生活在流程之外。很多边界情况很容易被忽视,比如任务运行两次时会怎样,或在提醒发送前记录刚好被修改会怎样。

工作流把时间当作正常步骤。你可以写明:等待 24 小时,发送提醒;再等待到总计 48 小时时升级给不同的审批人。系统会保存状态,所以截止规则不会隐藏在一个“cron + 查询”的角落里。

一个简单的审批规则可能是:

在报销单提交后,等待 1 天。如果状态仍然是“待处理”,给经理发消息。再过 2 天,如果仍为待处理,把任务重新分配给经理的上级并记录升级。

关键细节是在定时器触发时要先重新检查世界是否发生了变化:

  • 载入最新状态
  • 确认仍然是待处理
  • 确认当前受理人仍然有效(团队会变动)
  • 记录你做出的决定和原因

重试与失败恢复且不产生重复操作

Build durable approval workflows
Model a long-running approval flow with stored state, retries, and clear audit fields.
Try AppMaster

当某些操作因不可控原因失败时(支付网关超时、邮件服务返回临时错误,或应用在完成步骤 A 后崩溃导致步骤 B 未完成),会需要重试。危险很简单:重试时可能不小心把动作做两次。

在请求-响应模型中,常见模式是客户端调用一个端点、等待,如果没有得到明确成功就重试。为使其安全,服务器需要把重复调用视为同一意图。

一个实用的做法是使用幂等键:客户端发送类似 pay:invoice-583:attempt-1 的唯一 token。服务器为该键保存结果,并在重复请求时返回相同结果。这样可以防止重复扣款、重复工单或重复审批。

事件驱动工作流有另一类重复风险。事件通常至少投递一次(at-least-once),这意味着即便系统正常也可能收到重复事件。消费者需要去重:记录事件 ID(或业务键如 invoice_id + step)并忽略重复。工作流编排模式的核心差别在于:请求-响应关注的是调用的安全重放,而事件关注的是消息的安全重放。

无论哪种模型,下面几条重试规则都很有用:

  • 使用退避策略(例如 10s、30s、2m)
  • 设定最大尝试次数
  • 区分临时错误(重试)和永久错误(立即失败)
  • 把重复失败路由到“需要人工处理”的状态
  • 记录每次尝试,以便日后解释发生了什么

重试应在流程中显式定义,而不是隐藏的行为。这样才能让失败可见并可修复。

审计轨迹:让流程可以解释清楚

Store process state properly
Set up PostgreSQL data models for requests, decisions, deadlines, and correlation IDs.
Model Data

审计轨迹是你的“为什么”文件。当有人问“为什么这个报销被拒?”时,你应该能在几个月后不靠猜测就回答。这对事件驱动工作流和请求-响应 API 都很重要,但表现不同。

对于任何长期流程,都要记录能够重放整个故事的事实:

  • 执行者:谁做的(用户、服务或系统定时器)
  • 时间:何时发生(包含时区)
  • 输入:当时已知的信息(金额、供应商、政策阈值、审批情况)
  • 输出:发生了什么决策或动作(批准、拒绝、付款、重试)
  • 规则版本:使用的是哪一版策略/逻辑

事件驱动工作流能让审计更容易,因为每一步自然产生像 “ManagerApproved” 或 “PaymentFailed” 这样的事件。如果你把这些事件及其载荷和执行者一起保存,就能得到清晰的时间线。关键是让事件具有描述性,并把它们存到可以按案件查询的地方。

请求-响应 API 也能做到可审计,但故事往往分散在多个服务:一个端点记录“已批准”,另一个记录“已请求付款”,第三个记录“重试成功”。如果它们使用不同的格式或字段,审计就变成侦探活。

一个简单的修复是使用共享的“案件 ID”(也称关联 ID)。这是你附加到每个请求、事件和数据库记录上的标识符,例如 EXP-2026-00173,然后你就能跨步骤追踪整个流程。

选择合适的方法:优劣与权衡

最佳选择取决于你是需要立即得到答案,还是需要流程在数小时或数天内持续推进。

请求-响应适合工作短且规则简单的场景。用户提交表单,服务器校验、保存并返回成功或错误。它也适合明确的单步操作,如创建、更新或检查权限。

当"单次请求"悄然变成多步流程时,请求-响应就会带来痛点:等待审批、调用多个外部系统、处理超时或根据后续结果分支。你要么保持连接(脆弱),要么把等待和重试推到难以推理的后台任务里。

事件驱动工作流在流程是随时间展开的故事时更有优势。每一步都对新事件(批准、拒绝、定时器触发、支付失败)做出反应并决定下一步。这使得暂停、恢复、重试更容易,并且能保留清晰的原因记录。

存在真实的权衡:

  • 简单性 vs 持久性:请求-响应启动简单,事件驱动在长延迟下更可靠。
  • 调试方式:请求-响应沿直线跟踪,工作流则常需跨步骤追踪。
  • 工具与习惯:事件需要良好的日志、关联 ID 和清晰的状态模型。
  • 变更管理:工作流会进化并分支;当建模合理时事件驱动设计更易支持新路径。

一个实用例子:需要经理审批、再由财务复核并付款的报销。如果付款失败,你希望能安全重试且不重复付款。这种场景天然适合事件驱动。如果只是“提交报销”并做快速校验,请求-响应通常足够。

逐步指南:设计一个能在延迟中幸存的长期流程

Test your approval scenario
Prototype an expense approval with reminders, escalation, and payment retry logic.
Prototype Now

长期业务流程常以平凡的方式失败:浏览器标签关闭、服务器重启、审批卡了三天或支付提供商超时。从一开始就为这些延迟设计,无论你偏向哪种模型。

先定义一小组你能保存并恢复的状态。如果你不能指出数据库里当前的状态,那你就没有一个可恢复的工作流。

一个简单的设计顺序

  1. 设定边界:定义触发起点、结束条件和若干关键状态(待审批、已批准、已拒绝、已过期、已完成)。
  2. 命名事件和决策:写下随时间可能发生的事情(Submitted、Approved、Rejected、TimerFired、RetryScheduled)。保持事件名为过去式。
  3. 选择等待点:找出流程在哪些地方为人工、外部系统或截止而暂停。
  4. 为每步添加定时和重试规则:决定时间过去或调用失败时做什么(退避、最大尝试、升级、放弃)。
  5. 定义流程如何恢复:在每个事件或回调上,载入保存的状态,验证其仍然有效,然后推进到下一状态。

为能在重启中幸存,持久化你继续执行所需的最少数据:

  • 流程实例 ID 与当前状态
  • 谁可以下一步操作(受理人/角色)以及他们的决定
  • 截止时间(due_at、remind_at)和升级级别
  • 重试元数据(尝试次数、上次错误、下一次重试时间)
  • 针对副作用的幂等键或“已完成”标志(发送消息、扣款)

如果你能从存储的数据重构“我们在哪儿”和“接下来允许做什么”,延迟就不再可怕。

常见错误及规避方法

长期流程通常在真实用户到来后才露出问题。审批卡了两天,重试在错误时间触发,结果出现重复付款或审计缺失。

常见错误:

  • 把 HTTP 请求保持打开以等待人工审批。它会超时,占用服务器资源,并给用户一种假象“正在进行中”。
  • 重试调用但没有幂等性。网络抖动可能变成重复发票、重复邮件或重复“已批准”转换。
  • 不保存流程状态。如果状态存在内存中,重启会把它抹掉;如果只记录在日志里,就无法可靠继续。
  • 审计轨迹模糊。事件有不同的时钟和格式,导致在事故或合规审查时时间线不可信。
  • 把异步和同步混在一起却没有单一事实来源。一个系统说“已支付”,另一个说“待处理”,没人知道哪个是正确的。

一个简单的示例:报销在聊天里被批准,Webhook 延迟到达,支付 API 被重试。没有保存状态和幂等性,重试可能会多支付一次,而且记录里也解释不清为何如此。

大多数修复都回到显式化:

  • 在数据库中持久化状态转换(Requested、Approved、Rejected、Paid),并记录是谁/什么把它们改了。
  • 对每个外部副作用使用幂等键并保存结果。
  • 把“接受请求”与“完成工作”分开:快速返回,然后在后台完成工作流。
  • 标准化时间戳(UTC)、添加关联 ID,并同时记录请求和结果。

构建前的快速检查清单

Start with one workflow
Build one end-to-end process first, then reuse the pattern across your apps.
Get Started

长期工作更多是关于在延迟、人员和故障后保持正确,而不是某次完美调用。

写下对你流程来说“可安全继续”的含义。如果应用在中途重启,你应能从最后已知步骤继续而不靠猜测。

实用清单:

  • 定义流程在崩溃或部署后如何恢复:哪些状态被保存,下一步谁来运行?
  • 给每个实例一个唯一流程键(如 ExpenseRequest-10482)和清晰的状态模型(Submitted、Waiting for Manager、Approved、Paid、Failed)。
  • 把审批当作记录保存,而不是仅仅结果:谁批准或拒绝、时间以及理由或备注。
  • 绘出等待规则:提醒、截止、升级、过期。为每个定时器指定负责人(经理、财务、系统)。
  • 规划失败处理:重试必须受限且安全,并设置“需要人工复核”的停止点,供人员修正数据或批准重试。

一个理智的测试:想象在你已经向卡片扣款之后,支付提供商超时。你的设计应防止重复扣款,同时仍允许流程完成。

示例:带截止和付款重试的报销审批

Design events as a workflow
Turn your process into states and events using visual logic instead of fragile scripts.
Start Building

场景:员工提交一张 120 美元的出租车报销单。需要在 48 小时内获得经理审批。审批通过后系统向员工付款。如果付款失败,会安全重试并留下清晰记录。

请求-响应流程演练

在请求-响应模型中,应用往往像一个需要不断检查的对话。

员工点“提交”。服务器创建一条状态为“Pending approval”的报销记录并返回一个 ID。经理收到通知,但员工端通常需要轮询以查看状态是否改变,例如:“通过 ID 获取报销状态”。

为执行 48 小时截止,你要么运行一个定期扫描逾期请求的任务,要么在轮询时检查截止时间。如果任务延迟运行,用户会看到过期的状态。

当经理批准时,服务器把状态改为“Approved”并调用支付提供商。如果 Stripe 返回临时错误,服务器要决定现在重试、稍后重试还是直接失败。如果没有仔细的幂等策略,重试可能会导致重复付款。

事件驱动流程演练

在事件驱动模型中,每次变更都是一条记录事实。

员工提交,产生一个 “ExpenseSubmitted” 事件。工作流启动并等待 “ManagerApproved” 或 48 小时后触发的 “DeadlineReached” 定时事件。如果定时器先触发,工作流记录 “AutoRejected” 并说明原因。

批准到达时,工作流记录 “PayoutRequested” 并尝试付款。如果 Stripe 超时,它记录 “PayoutFailed” 和错误代码,安排一次重试(例如 15 分钟后),并通过幂等键确保只有在真正成功时才记录 “PayoutSucceeded”。

用户看到的状态保持简单:

  • 待审批(还剩 48 小时)
  • 已批准,正在付款中
  • 付款重试已安排
  • 已付款

审计时间线看起来像:已提交、已批准、检查截止、尝试付款、失败、重试、已付款。

下一步:把模型变成可工作的应用

选一个真实流程并端到端实现,然后再去泛化。报销审批、入职和退款处理是不错的起点,因为它们包含人工步骤、等待和失败路径。把目标设小:实现一条顺畅路径和两个最常见的异常分支。

把流程写成状态和事件,而不是界面。例如:“Submitted” -> “ManagerApproved” -> “PaymentRequested” -> “Paid”,并加入像 “ApprovalRejected” 或 “PaymentFailed” 之类的分支。当你清晰地看到等待点与副作用时,事件驱动工作流与请求-响应 API 的选择就变得实用。

决定把流程状态放在哪里。如果流程简单且能在一个地方强制更新,数据库就足够了。当你需要定时器、重试与分支时,工作流引擎能帮你跟踪接下来该做什么。

从第一天起就加入审计字段。保存谁做了什么、何时发生以及原因(注释或理由代码)。当有人问“为什么这笔付款被重试?”时,你要能不翻日志就给出明确答案。

如果你在无代码平台上构建这类工作流,AppMaster (appmaster.io) 是一个可选项,你可以在 PostgreSQL 中建模数据并以可视化方式构建流程逻辑,这有助于在 Web 和移动端保持审批与审计的一致性。

常见问题

何时应使用请求-响应而不是事件驱动的工作流?

使用请求-响应模型,当工作可以在用户等待的短时间内快速且可预测地完成时,例如创建记录或验证表单。需要在分钟到天级别、包含人工审批或需要定时、重试并在重启后安全恢复的流程,则适合事件驱动的工作流。

为什么把长期流程做成单个 API 调用会出问题?

长期任务不适合单次 HTTP 请求:连接会超时,服务器会重启,流程常常依赖人工或外部系统。把它当作一次调用,会导致丢失状态、重试产生重复项,以及为等待逻辑散落出大量后台脚本来处理。

如何让长期运行的流程在重启后可恢复?

一个好的默认做法是把清晰的流程状态持久化到数据库,并且仅通过显式的状态转换来推进流程。存储流程实例 ID、当前状态、下一个可操作的人或角色以及关键时间点,这样在部署、崩溃或延迟后都能安全恢复。

处理人工审批的最干净方式是什么?

把审批建模为一个暂停的步骤,当决策到达时再恢复,而不是阻塞或反复轮询。把每个决策记录为数据(谁决定、何时、同意/拒绝、理由),这样流程能可预测地向前推进,且便于审计。

为审批状态轮询是个坏主意吗?

轮询在简单场景下可行,但会增加噪音和延迟,客户端需要不断询问“完成了吗?”。更好的默认做法是变更时推送通知,让客户端按需刷新,而服务器保持状态的单一事实来源。

我该如何实现提醒、截止和升级机制?

把时间视为流程的一部分:存储截止时间和提醒时间,当定时器触发时重新检查当前状态再采取动作。这样可以避免在项已被批准后仍发送提醒,也能让升级在任务运行迟到或重复执行时保持一致。

重试时如何防止重复付款或重复邮件?

对所有外部副作用(如扣款或发送邮件)使用幂等键,并为该键保存结果。这样重试时会返回相同结果而不会重复执行同一动作,从而防止重复付款或重复邮件。

在事件驱动系统中如何处理重复事件?

假设消息可能会被投递多次,并让消费者具备去重能力。一个实用方法是存储事件 ID(或该步骤的业务键),遇到重复时忽略,从而避免重放触发相同动作两次。

长期工作流的审计轨迹应包含哪些内容?

记录一条事实时间线:执行者、时间戳、当时的输入、结果,以及使用的规则或策略版本。同时为该流程分配一个统一的案件或关联 ID,便于支持团队在不翻杂乱日志的情况下回答“它卡在哪里?”这个问题。

随着规则增长,审审批的数据模型如何保持清晰?

保留一个请求记录作为“案件”,把决策单独存储,并通过可重放的持久化转换来驱动状态改变。如果使用无代码工具(例如 AppMaster),可以在 PostgreSQL 中建模数据并用可视化方式实现步骤逻辑,从而保持审批、重试和审计字段在应用中一致。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧