P17

消息与异步架构

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 消息与异步架构要回答什么

零售订单 API 接收购物车、收货信息与支付方式,调用方希望尽快知道下单是否被受理。服务端还要检查价格、预留库存、发起支付、发送通知,并把结果交给履约系统。若这些动作都放在同次 HTTP 请求里,任何依赖变慢都会拉长响应;支付渠道短暂不可用时,即使订单数据库正常,用户也可能只看到超时。异步架构要回答的核心问题是:哪些结果必须现在确定,哪些工作可以稍后完成,以及等待期间由谁向用户解释状态。

本课沿用零售订单 API 的方案场景。假设日常下单流量约为每秒 80 个请求,促销时可达每秒 400 个;业务希望受理响应的第 95 百分位延迟低于 300 毫秒,常态下从受理到形成明确处理结果不超过 5 秒。这些是本课的设计输入,并非通用标准。“受理成功”表示系统已可靠记录处理责任;“下单成功”表示约定的库存与支付条件已经满足。若业务无法接受待处理状态,就不能仅靠改成异步来满足响应指标,必须重新讨论产品承诺。

消息是系统之间传递工作请求或事实的载体,队列则让生产者与消费者不必同时保持可用。订单服务提交消息后,库存或支付处理器可以按自身能力逐步处理。消息系统能缓冲短期流量差异,却不会创造无限处理能力;持续进入的工作多于完成的工作,积压仍会增长。异步也没有消除依赖,只是把时间上的依赖转化为状态、消息与恢复责任。

先把要求分成可测量的条款:受理接口的响应时间、最终结果的等待时间、允许的重复副作用、可接受的业务数据丢失范围,以及依赖故障时的处理方式。订单已受理后不能静默消失;同次支付意图不能重复扣款;用户能查询处理中、成功或失败。与“使用消息队列”相比,这些要求更能指导架构评审,也能在上线后判断方案是否兑现承诺。

零售订单的受理与后续处理边界
零售订单的受理与后续处理边界

设计选项至少包括同步串行调用、同步核心加异步通知、异步受理加后台处理。通知失败不应推翻已完成的订单,因此通常适合异步;价格合法性、身份与请求格式通常应在受理前检查。库存预留和支付是否也异步,要看超卖约束、支付体验与业务可接受的等待状态。本课选择异步受理方案来练习完整证据链,同时保留对其他选项的比较。架构选择必须说明适用条件,不能把异步当成所有延迟问题的默认答案。

2 命令、事件与编排边界

命令表达希望某个接收方执行的动作,例如“为订单预留库存”或“对支付意图发起扣款”。接收方可以拒绝命令,因为库存可能不足、支付凭据也可能失效。事件表达已经发生的事实,例如“库存已预留”“支付已确认”。命名时区分请求和事实,可以避免消费者把“支付已请求”误解成“支付已成功”。事件内容应描述产生时已经成立的事实,不能让后续查询结果悄悄改变它的原意。

在本课方案中,订单 API 完成基本校验后,保存状态为“处理中”的订单和待发送记录,随后返回订单标识与查询地址。后台发布器发送处理消息,订单编排器依次推进库存与支付。库存预留成功后才允许发起扣款;支付确认后,订单进入成功状态,再通知履约与用户。每个动作都要写清成功条件、失败条件及超时后的责任,不能仅靠箭头暗示处理顺序。

编排是由明确的协调方记录业务进度,并决定下一步命令;事件协作则让各参与方依据事实自行反应。编排有利于展示订单全貌,但协调方承担状态恢复责任;事件协作能减少直接调用关系,但链路分散后更难回答某个订单停在哪里。对当前入门案例,采用订单编排器管理库存与支付,采用事件通知履约和通知服务。这样既能清楚表达关键约束,也能让非关键订阅者独立演进。

从受理记录到库存、支付与结果事件
从受理记录到库存、支付与结果事件

数据库提交与消息发布之间存在容易忽略的空隙。先保存订单再直接发消息,进程可能在保存后退出,留下无人处理的订单;先发消息再保存订单,消费者可能找不到订单,或者数据库最终提交失败。可在同个本地数据库事务中保存订单与待发送记录,再由发布器反复扫描并发送。这种事务发件箱思路把“订单已保存就有发送责任”落在持久记录上。发布器仍可能重复发送,因此它解决的是遗漏风险,不等于消除了重复。

消息至少携带消息标识、订单标识、业务操作标识、事件类型、版本、发生时间与链路关联标识。消息标识用于识别同条消息的重复投递;业务操作标识用于识别相同支付意图,即使它被包装成多条消息也不能重复执行。关联标识帮助跨服务检索,但不应替代业务唯一性约束。只传必要字段,支付凭据等敏感信息应通过受控引用获取,避免把完整请求体长期散落在队列与日志里。

边界还包括数据归属。库存服务决定可用量与预留结果,支付服务维护支付意图和渠道结果,订单服务维护面向用户的订单进度。编排器可以记录这些事实的摘要,却不应绕过拥有者直接改库存表或支付表。消息格式也要允许演进:新增可选字段通常比改变旧字段含义安全;消费者遇到无法理解的必需字段时应明确隔离并告警。复杂的协议治理留待后续,本课先保证每个字段和每个状态都有负责人。

3 背压、重试与死信

背压是下游能力不足时,把压力反馈到上游的机制。队列可以暂存突发请求,但积压达到业务容忍上限后,入口仍要限制受理速度、暂时拒绝部分请求,或关闭非必要工作。这里的判断依据不只是队列长度,还包括最老消息年龄与预计等待时间。每条消息处理成本不同,长度相同的队列可能代表完全不同的等待体验;对订单业务而言,用户已等待多久往往比当前有多少条消息更重要。

消费者需要有明确的并发上限。若支付渠道只允许每秒处理 120 次,消费者扩容到每秒发起 400 次并不会提高有效吞吐量,反而可能触发限流与更多重试。可按渠道允许的速率调度支付工作,并把库存等其他工作分别观察。入口限流、队列缓冲与消费者并发是同个容量问题的不同控制点,评审时应画在同张责任图上,避免每个服务都把压力转交给后继服务。

重试适合短暂网络失败、临时限流等可能自行恢复的问题;库存不足、参数不合法通常应直接转为业务结果。对可能恢复的失败,要限定尝试次数与总时间,逐步延长间隔,并加入小幅随机变化,防止大量消费者同时重试。假设最多尝试 4 次,就应说明包含首次执行,并明确等待预算是否仍能满足最终结果目标。不能既承诺 5 秒内出结果,又配置持续数分钟的静默重试而不给用户状态解释。

消费者通常在完成本地业务提交后才确认消息。若先确认再提交,进程退出可能使工作丢失;若提交成功后确认丢失,消息会再次到达。因此必须把重复投递当作正常恢复路径。幂等的含义是对同个业务操作重复执行,最终业务效果保持相同,而不是声称函数只运行了 1 次。扣款、发券与库存扣减尤其要在持久记录和业务约束上实现这种性质。

死信是多次处理失败或无法解析的消息被隔离后的处理位置。它不是故障终点:需要记录失败原因、最后尝试时间、业务标识和负责团队,并建立告警与处置时限。修复代码后也不能无差别重放全部消息,必须先核对当前订单状态、支付状态与操作有效期。已取消订单的旧支付命令即使恢复可执行,也可能应该拒绝。死信保留和重放权限同样属于方案的一部分。

还有“结果未知”的失败。支付请求超时只表示本方没有收到答案,无法据此断言渠道没有扣款。此时应标记支付待确认,使用原支付意图查询或重试同个幂等请求;在证据不足时暂停冲突动作。库存释放和退款是业务补偿,也可能失败,需要各自的记录与重试责任。补偿不是让时间倒流,而是通过后续动作恢复业务可接受的状态,不能直接擦掉已经发生的支付事实。

4 动手:设计一次异步下单证据链

这次练习的交付物是可复查的设计证据,无需先部署真实消息集群。选定订单 ord-1701、客户端请求键 req-1701、支付意图 pay-1701,用状态表和时序记录串起整条链路。先写验收条件:重复提交相同请求只能得到同个订单;受理后即使发布器退出也能继续处理;重复支付消息只产生 1 笔有效扣款;查询接口最终能给出有依据的结果。所有例子只使用虚构标识和测试支付结果。

先记录受理动作的证据。客户端发送带请求键的下单请求,服务端校验后,在本地事务中保存订单、请求键映射与发件箱记录。事务成功后返回 202 Accepted、订单标识和查询地址;如果事务未完成,就不能返回已经受理。相同请求键带不同购物车内容应报冲突,不能偷偷覆盖旧订单。保存键的有效期限也要覆盖客户端可能重试的时间,否则过早清理会重新引入重复订单。

接着记录投递与处理证据。发件箱记录包含待发送、尝试时间及发送结果,发布器取得消息系统的约定确认后更新记录。编排器接收消息,检查订单当前状态与操作标识,再发出库存预留命令。库存结果、支付意图、支付结果与订单状态转换都要有时间和原因。避免把完整业务证据只放在会被采样或过期删除的日志里;业务状态记录负责恢复,日志与追踪负责解释执行过程。

围绕订单标识组织状态、消息与运行指标
围绕订单标识组织状态、消息与运行指标

然后注入 3 类故障,逐类写出预期证据。第 1 类是在订单事务提交后暂停发布器:应看到订单处理中、发件箱待发送,恢复发布器后链路继续。第 2 类是在消费者提交成功后模拟确认丢失:应看到再次投递,但重复检查阻止新的副作用。第 3 类是模拟支付响应超时:应看到支付待确认、订单保持处理中,并通过查询原支付意图取得真实结果。仅看到日志没有异常,不能证明这些条件成立。

证据表可以采用“时刻、订单状态、消息标识、操作标识、动作、结果”这 6 列。对每次故障保留故障前状态、注入点、恢复动作和最终业务记录。例如在确认丢失的用例中,同时检查消费者执行次数与支付流水:执行次数可为 2,成功扣款必须仍为 1。再把 API 响应与查询结果放在旁边,证明内部恢复没有把用户永久留在无解释的等待页。

最后补上运行指标:受理延迟、订单完成耗时、最老待处理订单年龄、发件箱未发送数量、消费失败率、重复拦截次数与死信数量。指标按链路阶段定位瓶颈,按订单标识查具体证据。对长时间处理中订单设置巡检任务,核对消息与业务状态并触发受控恢复。巡检不能仅靠重新发送支付命令解决所有问题,必须先查询已有支付意图。练习通过的标准是故障可恢复、恢复可解释,而且关键业务约束始终成立。

5 延迟与吞吐量取舍

吞吐量表示单位时间完成多少工作,延迟表示某项工作从开始到结束花费多久。异步能缩短受理请求占用连接的时间,却可能增加后台排队时间。评审至少区分受理延迟与业务完成延迟,并说明统计范围。若只展示 API 的 200 毫秒响应,却不展示订单在队列中等待了 40 秒,业务方会把系统的交付能力判断得过高。

做容量估算时,先用简化模型理解趋势。假设 1 条消息代表 1 个订单处理单元,输入速率为每秒 400 条,消费者完成速率为每秒 240 条,持续 60 秒将新增约 9600 条积压。这里暂不考虑重试和速率波动。若高峰结束后输入降至每秒 80 条,处理能力仍为每秒 240 条,净消化能力是每秒 160 条,清理这批积压约需 60 秒。不能用总处理能力直接除积压,因为新订单仍在进入。

并发数与吞吐量之间也有条件关系。若平均处理耗时为 0.2 秒,且任务独立、资源充足,48 个持续忙碌的执行槽位理论上约能完成每秒 240 次处理。这个值只是估算上限;数据库连接、渠道限流、消息获取效率都会使实际结果下降。若订单处理包含多个阶段,必须分别估算库存与支付能力,不能把最快阶段的吞吐量当作全链路能力。最终要通过接近真实业务比例的负载验证。

增加消费者能减少排队,但会增加计算资源、连接数与下游压力。批量处理可能提高吞吐量,却使低流量时的消息等待凑批;更频繁地轮询能缩短等待,却增加请求和空转成本。方案应优先满足用户承诺,再选择可接受的成本点。例如通知可以容忍更长等待,支付确认则要受更严格的年龄阈值控制。不同任务共用队列时,耗时通知也可能拖慢支付相关工作,需要结合优先级和容量隔离判断。

成本估算不仅包含消息存储和请求次数,还包括生产者、消费者、数据库、监控、备份及值班处置。若每个订单平均产生 6 条消息,则每日 10 万个订单对应约 60 万条业务消息,尚未计入重复投递与重试。保留 7 天意味着按这段时间的消息体和元数据估算存储,而不是只乘当天流量。计费口径可能包含投递次数与副本,必须用实际服务的计价单位核算,不能用消息条数直接等同账单。

容量上限最终要转成业务动作。当预计完成时间越过承诺时,系统可以停止受理部分订单、提示延迟,或对低优先级工作降速。新增订单是否应继续占用库存,也要与业务协商。简单地放大队列容量可能让更多用户等得更久。异步方案的价值在于把突发流量处理得可控,而可控必须包括主动降载、可见状态与清晰的恢复顺序。

6 常见坑:同步等待异步完成

常见误区是在 API 里发出消息后,仍阻塞等待消费者返回最终结果。这样既保留了长请求占用,又增加了消息投递和关联响应的环节。如果消费者在 5 秒后完成,而入口在 3 秒时超时,用户仍不知道订单是否成功。客户端再次提交时还会引入重复风险。若最终结果必须在当前请求内返回,应直接评估同步方案是否更简单,并使用请求幂等和明确的超时边界。

异步受理接口应清楚约定状态查询。返回的处理中状态要有订单标识、查询入口和必要的下一步提示;客户端可以按逐步放缓的频率查询,也可以在通知到来后查询权威结果。通知可能重复或丢失,因此不能只靠手机推送决定订单是否成功。用户刷新页面、换设备或退出后再回来,都应通过同个订单资源取得状态,不要求保存某次短连接中的临时答案。

另个常见问题是把消息系统的确认当成业务完成。发布确认只说明消息系统按照其配置接下了消息;消费确认说明消费者按协议确认了处理。它们都不自动证明库存和支付满足了订单成功条件。仪表盘应分别展示发布、消费与业务完成,告警也应覆盖“队列已空但订单仍处理中”的情况。否则消息层看似健康,业务却可能已停在错误的状态分支。

从用户等待反查业务状态与消息处理证据
从用户等待反查业务状态与消息处理证据

排查时从用户可见状态出发。如果订单不存在,先检查受理事务和客户端请求键;若订单存在但没有待发送记录,检查本地事务边界;若记录待发送,检查发布器及发送错误;若已投递却没有状态推进,检查消费失败、重复拦截与业务拒绝;若支付待确认,查询原支付意图。每个分支都要保留恢复入口,避免运维人员靠手改数据库跨越约束。

超时也需要分层。入口响应超时、消息等待超时、单次调用超时和业务处理截止时间各自控制不同范围,不能共享含义模糊的 timeout。业务到期之后,晚到消息仍应检查当前状态和有效期;取消订单后再收到库存预留成功事件,应触发释放预留等后续处置,而非直接恢复支付。入门阶段先把状态与截止时间画清楚,比增加更多后台线程更有助于解决等待问题。

7 案例:重复事件导致重复扣款

促销期间,订单 ord-1701 的库存预留完成,编排器发送支付命令。支付消费者调用渠道成功,写入本地支付结果后,在确认消息之前退出。消息随后被再次投递。旧实现为每次消费生成新的支付流水号,并直接调用渠道,结果同个订单出现 2 笔扣款。消息系统没有违反至少投递 1 次的约定,真正缺失的是业务操作的稳定身份,以及跨故障恢复时的重复保护。

先确定影响范围,再恢复业务。暂停有问题的支付消费路径,保留原始消息、支付意图和渠道流水,核对订单金额与实际扣款。已经重复扣款的订单进入受控退款与用户解释流程,退款也需要稳定操作标识及结果查询。不要删除第 2 笔流水来制造只有 1 笔的假象。恢复前,应确认哪些请求从未发出、哪些结果未知、哪些已经明确成功,避免批量重放再次扩大影响。

修复时,为这次收款建立稳定的 pay-1701 支付意图,并在业务层约束其唯一性。消费者首次处理时创建或读取该意图,向支持幂等的支付接口传递相同操作键;再次收到命令时,读取已有状态,成功则返回已有结果,处理中则查询渠道,明确失败才按业务规则决定后续动作。消息标识只能拦截同条消息,稳定支付意图还能覆盖发布器生成新消息标识的重复业务请求。

本地数据库的去重记录必须与相关本地状态变更放在同个事务中,否则去重成功而业务没提交会造成漏处理。但本地事务无法自动覆盖外部支付调用:如果渠道已经扣款而本地结果没保存,恢复后仍面临结果未知。此时需要渠道幂等接口、按原操作查询,以及定期对账。若渠道不提供这些能力,方案必须承认重复与人工核对风险,不能仅用“已加去重表”宣称端到端只扣款 1 次。

顺序问题也会放大重复影响。较晚到达的“支付处理中”事件不应把已成功状态覆盖回处理中,取消与支付成功同时发生时也要按业务规则确定处理。可检查当前状态是否允许转换,并保留拒绝旧事件的原因。本课不展开分布式事务与复杂顺序协议,只要求评审者能看出:重复、晚到与结果未知分别由哪些记录和规则处理。

修复验证应重放同条消息、发送不同消息标识但相同支付意图的命令,并模拟渠道成功后本地写入失败。验收观察实际支付记录、订单状态和对账差异,而非只统计函数调用次数。随后逐步恢复消费,限制速率并监控重复拦截与未知结果的数量。事故给架构留下的决策依据是:消息投递保障不能代替业务幂等,关键外部副作用必须具备可查询、可核对和可恢复的证据。

8 把异步边界写进业务方案

架构决策记录,即 ADR,应先描述问题与约束:促销突发使同步链路不稳定,业务允许订单短暂处理中,但不能接受静默丢单和重复扣款。比较同步串行、仅通知异步、核心流程异步这 3 个选项,说明各自的用户体验、开发复杂度与恢复成本。选择核心流程异步的理由应建立在业务容忍等待与可运营的状态模型上,并列出代价:更多持久记录、消息治理、巡检与故障处置工作。

方案中的异步边界表可按“动作、拥有者、输入、完成条件、超时动作、幂等键”编写。受理由订单服务负责,以订单和发件箱提交为完成条件;库存以预留记录为证据;支付以稳定支付意图及渠道结果为证据;通知失败只影响通知状态。另附状态流转图和用户文案,让产品、开发与值班人员对“处理中”具有相同理解。无法写清完成条件的动作,不适合直接交给消费者自由发挥。

高可用设计应覆盖入口、订单数据库、消息系统、发布器和消费者。部署多个消费者前,要确认它们共享持久幂等约束,不能依赖进程内存去重。消息系统的副本、确认和持久化配置要对应可接受的数据丢失范围;应用多副本也不能弥补消息存储的单点。分区或短暂故障时,是停止新受理还是允许可靠落入发件箱后等待,应由容量和业务承诺共同决定。

跨可用区部署有助于应对局部故障,但仍需演练切换、积压恢复和数据库恢复。恢复时间目标表示期望多久恢复服务,恢复点目标表示可容忍丢失多长时间的数据;对于已受理订单,应明确实际配置与备份策略能否支撑要求。数据库从旧备份恢复后可能缺少去重记录,而渠道扣款仍然存在,因此恢复流程必须包含支付对账。不能把“服务重新启动”视为业务已经恢复一致。

最后把容量、成本与风险并列评审。给出常态和促销流量下的处理预算、最老消息告警阈值、保留时间、重试上限及死信负责人。风险清单至少覆盖重复副作用、消息遗漏、长期积压、旧消息误执行与敏感信息扩散,每项对应可操作的控制和证据。注明重新评估的触发条件,例如订单完成延迟持续超标、支付渠道限额变化或人工处置数量上升,让 ADR 能随业务发展更新。

9 从消息与异步架构走向数据架构(P18)

异步链路会产生多份数据视图:订单库显示处理中,支付系统已扣款,查询页面的汇总视图可能还没更新。这些差异不一定意味着数据损坏,但必须有可解释的传播时间和明确的权威来源。支付事实由支付记录与渠道证据确认,订单业务结果由订单状态规则确认,页面汇总只是面向查询的视图。界面应允许短暂延迟,同时避免把过期视图用于再次扣款等关键决策。

因此,消息边界会自然引出数据边界。订单、库存与支付分别拥有哪些字段,哪些数据需要强约束地一起修改,哪些可以通过事件稍后同步,都会影响后续数据架构。把所有数据复制给所有服务会增加一致性和权限问题;每次查询都跨服务拼接又会重新引入同步依赖。P18 将继续讨论这些选择,本课先保留每项事实的来源、更新时间与可接受延迟。

事件保留也不等于业务数据可以永久重建。事件可能只记录摘要,格式会演进,隐私规则也可能要求删除部分信息;若没有设计完整的历史与重放规则,不能假设清空数据库后重播消息即可恢复。当前方案应明确订单与支付记录的保存责任、消息的有限保留期,以及巡检和对账所依赖的数据。完整事件溯源、复杂一致性协议与跨地域多写属于进阶议题,此处只建立问题边界。

进入 P18 前,带上本课产出的 ADR、状态图、消息字段表和故障证据表,并为每个业务字段标出拥有者。再挑出“订单已受理但消息未发”“渠道已扣款但本地未知”“页面尚未更新”这 3 个时刻,说明各自应查询什么、允许等待多久、由谁修复。能够回答这些问题,才说明异步流程与数据责任接上了,而不只是画出了若干队列与箭头。

放进方案里

本零售订单 API 采用异步受理:入口完成身份、请求格式和价格有效性检查,在本地事务中保存订单、请求键映射与待发送记录,提交后返回订单标识和查询地址。订单处理中表示系统已经接下处理责任,成功表示库存预留与支付确认均满足约定。通知和履约订阅成功事件,订单编排器负责推进库存与支付,并记录失败原因和补偿进度。

方案以每秒 80 个常态请求、每秒 400 个促销请求作为初始容量输入,目标受理延迟第 95 百分位低于 300 毫秒,常态完成时间不超过 5 秒。压测分别验证库存和支付处理能力,根据最老消息年龄控制受理速率。成本估算包含消息投递与保留、消费者、数据库、监控及人工处置,不能仅计算队列费用。持续积压超过业务等待预算时,入口降载并向用户解释延迟。

可靠性采用事务发件箱、持久幂等记录与稳定支付意图;消息提交后确认,允许重复投递并阻止重复副作用。支付超时进入待确认,通过原操作查询与对账恢复。重试限定次数和时间,无法自动恢复的消息进入隔离区,由明确负责人核对后重放。库存不足等业务拒绝直接形成结果,不进入无限重试。

入口、发布器和消费者部署多个实例,数据库与消息系统按业务恢复目标配置持久化、备份及故障切换。演练覆盖提交后停机、确认丢失、支付结果未知和恢复后的对账,验收检查实际订单与支付记录。ADR 保留同步方案的比较、异步等待的体验代价、死信处置责任和重新评估条件;数据归属、查询视图与保存周期作为 P18 的后续设计输入。

10 小结:下一步数据架构(P18)

消息把工作交给未来处理,系统就必须同时交付可靠受理、可查状态和恢复责任。命令表达意图,事件表达事实;背压限制压力,重试处理暂时故障,死信保留待处置工作。投递重复需要业务幂等,外部结果未知需要查询与对账,高可用还要覆盖业务恢复。

自测时可解释这 3 个问题:发布确认为什么不代表订单成功?消费者提交后退出为什么可能再次收到消息?支付超时为什么不能直接换个操作键重扣?若能用本课订单的记录与状态回答,就可以进入 P18,继续决定数据由谁拥有、怎样保存,以及查询视图如何反映异步产生的事实。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务