P5

架构权衡

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

课内目录

1 为什么先学「架构权衡」

零售应用里的「提交订单」看起来只是按下按钮,服务端却要回答许多问题:商品是否仍可售,价格是否有效,库存能否保留,订单是否已经创建,支付结果何时可知。任何环节变慢,用户都会感到卡顿;任何环节重复执行,都可能带来重复订单、重复扣款或库存误扣。架构的任务,是把这些工作放在合适的位置,并规定出问题时如何继续。

假设促销期间,每秒有 300 个创建订单请求,平时每秒只有 30 个。业务希望大部分请求在 800 毫秒内得到明确答复,同时不能超卖,也不能因为用户连续点击而产生重复订单。开发团队只有 4 人,还要承担值班。这里至少存在响应速度、业务正确性、可用性、费用和维护负担等目标。把目标全部写成「越高越好」,并不能指导设计。

所谓权衡,就是在明确约束的前提下,选择更重视哪些目标,接受哪些代价,并安排证据证明选择有效。例如,把短信通知移出创建订单的同步链路,可以缩短等待时间;代价是短信可能稍后才到,还需要处理通知失败。这个选择通常合理,因为「订单已经保存」和「短信已经送达」不是同样紧迫的业务事实。

但把库存确认也直接移出同步链路,含义就不同了。若页面先显示「购买成功」,稍后才发现没有库存,系统其实把库存不足的风险转给了用户和客服。异步并没有消除问题,只是改变了问题出现的时间、位置和承担者。讨论架构时,需要问清楚:用户收到的承诺是什么,系统何时有证据支撑这个承诺。

本课始终使用同一个简化场景:客户端调用订单 API,订单服务访问库存、订单数据库和支付渠道,并发送通知。创建订单与支付完成是不同阶段,课程不假定所有环节必须拆成独立服务。即便它们位于同一个应用进程中,数据边界、失败边界和返回语义仍然需要设计;即便拆成许多服务,这些责任也不会自动清晰。

订单 API 的目标、约束与结果
订单 API 的目标、约束与结果

观察图中的目标与约束:目标可以讨论优先级,约束则决定哪些方案不可接受。比如「不能重复扣款」通常属于底线;「通知在 5 秒内到达」可能属于可协商目标。遇到冲突时,应先保护底线,再优化体验,而不是把所有指标平均分配。不同业务会划出不同底线,因此不存在脱离场景的通用最优架构。

学习权衡也有助于识别貌似完整的方案。只写「使用缓存提高性能」,没有说明缓存什么、允许多旧、失效后怎么办,就还不能实施。只写「采用重试提高可靠性」,没有说明哪些请求可以重试,也可能放大故障。技术名称只能提示可选工具;真正的方案要说明触发条件、行为变化、业务后果和检查办法。

读完本课,你应能把「快、稳、省」改写成可验证的要求,画出订单成功与失败的路径,判断哪些动作适合同步完成,哪些可以延后,并用日志、指标和业务记录组织证据。我们只讨论入门所需的机制,不推导分布式协议。重要的是建立提问顺序:先明确承诺,再定位状态,最后选择机制和验证方式。

2 核心概念与边界

目标先写成可观察的句子

性能通常包含延迟和吞吐量。延迟是请求从发出到获得结果所用的时间,吞吐量是单位时间完成的工作量。平均延迟容易掩盖少量慢请求,因此常看 p95:把请求耗时排序,约 95% 的请求不超过这个值。若创建订单的 p95 是 800 毫秒,并不意味着每个用户都能在 800 毫秒内完成,更不意味着支付也已经结束。

可用性表示服务在规定条件下完成规定功能的能力。必须先定义「规定功能」:返回网页、接受下单请求、创建可查询的订单,是不同标准。库存不足是正常业务拒绝,不宜直接算作系统故障;服务超时却可能导致用户无法判断结果。只统计 HTTP 状态码,会遗漏这类体验差异。指标口径需要与接口承诺保持一致。

一致性在本课中指相关数据与业务规则是否相符,以及变化何时对外可见。比如库存不能因并发请求被扣成负数,订单金额应对应用户确认的价格版本。不同数据可以有不同要求:商品介绍允许短暂陈旧,支付状态则不能凭旧缓存宣布成功。说「系统允许最终一致」过于笼统,需要具体到字段、动作和可接受等待时间。

边界决定谁负责证明事实

订单服务应是订单状态的权威来源,库存模块负责库存占用规则,支付渠道负责外部支付事实。权威来源指发生冲突时以谁的记录为准,并不意味着只保留 1 份数据。搜索索引和统计报表也可以保存订单副本,但它们适合查询和分析,不能直接覆盖原始订单状态。副本刷新晚了,首先要修复刷新过程,而不是随意改写业务事实。

数据库事务能让约定范围内的多项修改共同提交或撤销。例如同库中的订单记录和待发送事件可以放在同一个事务中。事务不能自然覆盖已经发出的短信,也不能靠订单数据库回滚外部扣款。只要动作跨出了这个范围,就必须考虑「本地已经成功,外部结果未知」这样的中间状态,并为其安排后续确认。

订单事实、事务范围与外部动作
订单事实、事务范围与外部动作

成本也不只指机器费用。缓存增加了数据失效规则,消息队列增加了积压与消费维护,拆分服务增加了调用与发布协调。对 4 人团队来说,复杂故障能否在夜间被理解,也是设计约束。较简单的单体应用配合明确模块和数据库事务,可能比过早拆分更合适。选择简单方案仍要记录增长边界,以免把当前适用误认为永久适用。

可以用 3 类内容整理需求:不能违反的业务规则、希望达到的服务目标、暂时采用的工作假设。例如「同一购买意图只创建 1 个订单」是规则,「p95 不超过 800 毫秒」是目标,「峰值持续不超过 10 分钟」是假设。规则需要机制保障,目标需要测量,假设需要在现实变化时重新检查。这种分类比罗列技术组件更接近架构决策。

最后要区分故障与不确定性。明确收到「库存不足」时,系统知道没有占用成功;调用超时时,只知道自己没有及时收到结果。对方可能未执行,也可能已经执行但响应丢失。把超时直接当作失败,是许多重复操作的起点。接下来的机制都围绕这个边界展开:限制等待、识别重复、保留事实,并让未知结果最终可以查清。

3 关键机制(可迁移)

同步等待与异步处理

同步调用意味着当前请求要等到下游给出结果,才能继续相应步骤;异步处理则允许先记录工作,再由后续执行者完成。同步容易表达「现在已经完成」的承诺,但依赖越多,请求越容易被其中的慢环节拖住。异步可以缩短前台等待,却需要展示处理中状态、保存待办工作,并处理重复、延迟和永久失败。

判断动作能否异步化,可以先问:缺少它的结果,当前响应还能真实吗?订单确认前必须知道的价格校验通常留在同步路径;通知发送通常可以延后。库存是否可以延后,取决于接口是否只承诺「已收到申请」。若对外承诺「已创建并保留库存」,就必须先获得相应证据。接口语义与实现路径必须同时调整,不能只移动代码。

超时限制等待,幂等限制重复副作用

超时是调用方愿意等待的上限,不是撤销执行的命令。入口的总等待预算应留出本地处理和返回时间,再分给下游。若入口只等 800 毫秒,下游却允许等待 3 秒,入口结束后仍可能有工作占用资源。预算也不应机械平均分配:数据库写入、库存确认和非必要查询的重要性不同,应分别设置并用测量修正。

重试适用于部分暂时性失败,但不能无限进行。对明确的库存不足,重试通常没有意义;对连接短暂中断,可以在剩余预算内有限重试。退避是让后续尝试等待更久,随机抖动则让大量客户端不在同一时刻再次发起请求。它们能降低同时冲击,却不能替代容量规划。故障期间若入口、服务和客户端各自重试,实际请求量会被层层放大。

幂等表示同一个业务操作被重复请求时,不产生额外的业务效果。创建订单可由客户端为同一购买意图生成幂等键,服务端将键、请求摘要和订单结果持久化,并用数据库唯一约束解决并发竞争。相同键携带不同商品或金额时应拒绝复用;用户开始新的购买意图时应使用新键。只在进程内保存键,重启或多实例运行时就可能失效。

缓冲能吸收波动,也会积累等待

队列把工作从生产者传给消费者,可以吸收短时间突增,但不会凭空增加下游处理能力。若每秒进入 300 条任务,而消费者每秒只能完成 200 条,持续 60 秒就会增加约 6000 条积压。若之后到达量仍接近消费能力,积压会清得很慢。用户关心的通常是任务等了多久,因此除了队列长度,还应观察最老任务的年龄。

到达、处理能力与积压的关系
到达、处理能力与积压的关系

当到达速度超过处理能力时,需要背压:让上游减速、限制并发,或明确拒绝部分新请求。限流保护入口,熔断在连续下游失败时暂时停止部分调用,降级则提供范围更小但真实的功能。例如关闭推荐不影响创建订单;跳过库存校验却可能破坏业务底线。这些机制的共同点,是有意识地限制影响范围,而不是假装所有功能仍然正常。

交互中使用固定处理能力,只改变允许进入的请求量。注意观察:低于容量时,拒绝可能增加,但积压受控;高于容量时,短时间内看似接受了更多请求,等待却不断增长。真实系统的容量会随数据库争用和请求类型变化,因此这个模型用于理解方向。生产阈值必须来自代表性压测,并在接近饱和之前留出余量。

4 关键机制续:落到订单链路

现在把机制放进具体流程。客户端提交商品、数量、报价版本和幂等键。订单服务先校验身份与请求格式,再查询是否已有该键对应的结果。若结果已存在,就返回同一个订单;若仍在处理,就返回约定的处理中状态或查询入口。不要在无法确定旧操作状态时,直接创建另一个订单来「解决」超时问题。

接着检查价格与可售状态。页面可以用缓存快速展示商品信息,但提交时应按明确规则验证报价是否仍有效。价格变化时,要让用户重新确认或执行事先约定的价格承诺。不能在用户不知情的情况下替换成交金额。架构讨论到这里,已经与产品规则相连:缓存时间并不是纯粹的技术参数,它决定用户可能看到多旧的信息。

库存模块接收占用请求,依据原子更新或等价的并发控制,保证剩余量不足时不再成功占用。库存占用也要带稳定的业务标识,以便重复调用返回已有结果。若它是独立服务,订单数据库事务无法回滚该占用,因此需要占用凭据、有效期和明确的释放操作。此时系统会出现短暂的中间状态,不能靠隐藏状态来回避它。

订单服务拿到占用凭据后,在本地事务中保存待支付订单、幂等结果以及待发布的订单事件。把事件与订单一起保存,可以避免「订单已经提交,进程却在发消息前崩溃」导致通知永久丢失。后台任务随后发布事件,并在得到确认后标记进度。发布确认也可能丢失,因此事件可能被重复投递,消费者仍需按事件标识去重。

创建订单、支付确认与通知的路径
创建订单、支付确认与通知的路径

客户端得到的成功响应应明确表示「待支付订单已创建」,并包含订单标识、当前状态和查询位置。支付请求及回调另有流程。发起支付时沿用稳定的支付业务标识,回调时检查来源、金额、订单关联和当前状态,再按允许的状态转换更新订单。网络通知到达不代表可以无条件将任何订单改成已支付,重复或迟到消息也必须受规则约束。

库存已占用而订单保存失败时,需要释放库存。释放本身也可能失败,因此应该记录可追踪的待处理事项,反复确认直到完成或转人工。补偿不是把时间倒回去,而是执行新的纠正动作。如果库存占用在订单记录之前完成,应让库存侧保留可查询凭据与到期机制,并安排核对,避免进程崩溃后只剩无法定位的占用。

待支付订单过期与支付成功也可能同时发生。入门设计至少应写清允许哪些状态转换、谁拥有最终裁决权,以及冲突转入什么核对流程。不能仅靠「先收到哪个消息」判断真实支付先后。若已经付款却无法履约,需要根据业务规则退款或人工处理。跨系统状态不确定时,宁可明确显示处理中,也不要编造已完成或已取消的确定结论。

这套流程仍可运行在模块清晰的单体中。只有当团队边界、扩容需求或故障隔离收益足够明确时,才考虑把模块拆为服务。拆分后会增加网络超时、独立发布和跨库状态协调;保留单体也可能遇到共同资源争用。权衡点不在组件数量,而在收益能否覆盖新增的失败路径,并且团队是否有能力观察和恢复这些路径。

5 动手与命令/控制台直觉

动手时,先用可重置的练习环境建立小基线:商品剩余 10 件,购买数量为 1,创建接口只返回待支付订单。记录订单数量和库存占用,再发送请求。下面的地址代表练习机上的接口;字段名称只是示例。练习凭据和用户身份按环境约定提供,不要把真实支付或生产账号接入这种故障实验。

curl -i --max-time 2 \
  -X POST http://localhost:8080/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: purchase-demo-001' \
  --data '{"sku":"SKU-100","quantity":1,"quoteVersion":"q17"}'

重点观察响应表达的事实。首次请求可以返回 201 和待支付状态;同键重放可以按契约返回 200 或已有结果。状态码并非唯一答案,订单标识保持一致、库存只占用 1 次才是业务验证。把数量改为 2 却保留原键时,预期应为明确冲突,而不是返回旧订单假装新参数已被处理。

接着测试「客户端没收到结果」。在练习服务中让响应发送阶段延迟超过 2 秒,再执行同样的命令。客户端超时后,先按幂等键或订单查询接口查状态,再用原键重放请求。若得到同一个订单,就说明重复提交被关联到了已有操作。仅看到命令报超时,不能证明数据库没有写入;需要把客户端现象与服务端记录分开观察。

再测试并发与库存边界。让多个练习请求竞争最后 1 件商品,使用不同的购买意图键。预期只有允许数量的占用成功,其余收到库存不足或约定的处理中结果。这个实验检验的是数据库或库存模块的原子约束。先读剩余量、在应用里减去数量、再写回的流程,如果缺少并发保护,单个请求看起来正确,并发时仍可能超卖。

打开控制台时,可先寻找 4 组信息:入口请求量与响应耗时,应用正在处理的请求数,数据库连接等待与事务耗时,后台队列长度与最老任务年龄。看到 CPU 空闲,不代表系统有余力;线程可能正在等待连接或远程响应。也不要因为队列长度下降就立刻判断恢复,可能是消息过期或被丢弃,需要同时查看消费成功和失败数量。

做故障注入时,每轮只改变 1 个条件,例如延迟库存响应、暂停通知消费者,或让消息重复到达。事先写下预期:订单是否还能创建,哪些状态会停留,哪个告警应触发,以及如何恢复。实验结束后检查业务记录是否收敛。成功标准不能只有「页面能打开」,还应包括库存占用无泄漏、重复消息无重复副作用、处理中状态可被查清。

最后比较 2 个方案时,要保持请求分布、数据量和观察窗口一致。某方案在空数据库、单用户下更快,并不能证明它在促销峰值下更好。记录每轮输入、配置版本和结果,才能解释差异。小规模实验的价值是发现边界与失败行为;由它推到生产容量时,还需要考虑热点商品、慢查询、网络波动和真实支付行为带来的变化。

6 观测:看哪些字段与信号

观测的目的,是把「用户说下单失败」拆成可以核对的事实。请求标识用于追踪某次调用,订单标识关联业务实体,幂等键关联同一购买意图的重复尝试,事件标识关联异步处理。它们不能互相替代:用户重试时会产生新的请求标识,但应保留原来的幂等键;同一个订单则可能产生多个不同事件。

日志可以记录时间、接口、请求标识、链路标识、订单标识、处理阶段、耗时、结果类别、依赖名称和尝试次数。幂等键按需求保存摘要或受控值,避免原始值进入过多系统。不要记录支付凭据、完整银行卡信息或无必要的个人资料。日志应足以解释决策,同时控制敏感数据暴露;业务详情优先通过受权限保护的记录查询。

结果类别应区分业务拒绝、系统错误和未知结果。库存不足说明规则正常生效;数据库连接超时可能影响下单能力;支付请求超时则需要继续核对外部状态。若全部写成「失败」,值班者会把正常拒绝当故障,或者把真正的不确定性忽略掉。分类并非为了美化成功率,而是让每类结果对应正确的处置动作。

从用户症状关联指标、链路与业务记录
从用户症状关联指标、链路与业务记录

指标适合看总体趋势:创建订单请求量、系统错误比例、p95 延迟、在途请求数、库存调用超时数、幂等重放数、连接等待时长以及队列最老任务年龄。不要把订单标识作为指标标签,否则几乎每个订单都会产生新的序列,增加存储与查询负担。单个订单的细节放在日志或链路记录中,指标保留接口、结果类别等数量受控的维度。

链路追踪把入口与下游调用串起来,帮助判断等待发生在哪里。例如入口总耗时 1200 毫秒,其中库存调用占 900 毫秒,数据库写入占 80 毫秒,排查就应先聚焦库存依赖。追踪采样可能遗漏个别请求,时钟误差也会影响跨机器时间判断。因此链路适合定位方向,最终业务状态仍要回到订单、库存和支付记录核对。

告警要对应用户影响与行动。创建订单系统错误持续升高,应引导检查最近变更和共同依赖;通知任务最老年龄超过约定时,应检查消费者与通知渠道。短暂的 CPU 峰值未必值得夜间叫醒人员,而少量重复扣款即便比例很低也需要立即处理。阈值来自业务底线、基线与处理能力,不能从别的系统照搬。

还要补充业务核对信号,例如长时间停留在支付确认中的订单、已付款但没有可履约订单的记录、过期后仍未释放的库存占用。技术指标全部正常,仍可能存在这些业务错误。可以定期执行受控核对,将差异转为有负责人、有处理时限的事项。自动核对负责发现不一致,是否自动修复则取决于证据充分程度与动作可逆性。

观测本身也有成本和边界。记录每个细节会增加写入量,完全依赖采样又可能遗漏罕见问题。可对常规成功请求采样,对关键状态变化保留必要的持久记录,并为错误请求提供更完整的诊断线索。架构权衡在这里再次出现:要保存足够证据,但不把日志当成无限容量的业务数据库,也不让观测链路成为下单的脆弱依赖。

7 常见坑与反模式

用组件名称代替问题定义

「加缓存、上队列、拆服务」是常见开场,却不是结论。缓存适合减少重复读取,但价格或库存读得更快不等于规则更正确;队列适合解耦可延后的任务,但会增加等待和恢复逻辑;拆服务能建立部署边界,却会增加网络与状态协调。先写出实际瓶颈和期望变化,再选择组件,才能判断引入它是否值得。

把超时等同于失败,把重试当成默认动作

创建订单超时后换新键重发,可能创建多个订单。支付超时后直接再次扣款,风险更高。正确做法是保留稳定业务标识,查询或重放同一操作,并让服务端限制重复效果。有限重试还应有总预算和可重试错误范围。对明显超载的服务密集重试,只会让原有请求更慢,让更多请求进入超时与重试循环。

用无限队列隐藏过载

入口始终返回已接收,看起来没有拒绝,但用户可能等待很久,系统也可能需要数小时清空任务。接受工作本身就是承诺,应规定最大等待、过期处理和查询方式。队列容量、消费者能力与入口准入要配套设计。达到保护阈值时,明确告诉用户稍后重试,往往比接收无法按约定完成的工作更诚实,也更容易恢复。

只追求总体可用,忽略数据底线

依赖故障时跳过库存检查,可以提高接口的表面成功率,却可能产生无法履约的订单。降级必须写出仍能保证的事实:可以展示缓存商品介绍,可以保留购物车,也可以暂停某类下单。无法证明库存已保留,就不能返回相应成功承诺。保护核心规则有时会降低短期可用性,这是应当明确记录的业务选择。

以为异步消息只会到达 1 次

生产者可能因确认丢失再次发送,消费者也可能在完成动作后、确认消息前崩溃。重复是必须准备的正常情况。消费者应把去重记录与本地业务修改放在适当的原子边界内;若要调用外部系统,还需外部幂等标识或查询核对。先记「已处理」再做动作可能丢工作,做完后随手记标记又可能重复,执行顺序需要结合边界分析。

把补偿和回滚混为一谈

释放库存、退款和取消通知都是新的业务动作,可能失败,也可能无法完全消除原动作的影响。用户已经收到的短信无法被数据库回滚收回。方案应为补偿记录状态、重试条件和人工入口,同时允许核对纠正。只画成功路径、在失败箭头旁写「回滚」,不足以证明跨系统流程可以安全恢复。

从约束到机制,再到验证与复审
从约束到机制,再到验证与复审

另一个隐蔽问题,是把临时经验固定成永久阈值。例如早期每秒 50 个请求运行良好,就把 50 当作所有时段、所有商品的安全容量。数据增长、热点变化和新功能都会改变处理能力。应保留阈值依据、适用假设与复审条件。架构决策记录可以很短,但要让后来者看出为何这样选,以及出现什么证据时应该重新讨论。

8 案例:订单系统相关故障

下面是教学用故障案例。某零售应用促销开始后,创建订单请求从每秒 30 个升至每秒 300 个。订单服务同步等待库存、数据库写入和通知发送。通知渠道平时约 100 毫秒返回,这次延迟升到 3 秒。客户端只等 1 秒,超时后自动提交新请求,而且每次都生成新的幂等键。用户陆续反馈页面转圈、出现多个待支付订单。

最初的仪表盘显示订单服务 CPU 只有约 40%,团队怀疑网络偶发波动,增加了应用实例。请求成功接入的速度随之提高,但更多请求同时占住数据库连接,连接等待和超时继续上升。扩容缓解了入口计算压力,却没有消除通知慢、重复提交与数据库并发争用。瓶颈被推进下游,整体体验没有按预期改善。

当班人员抽取投诉对应的请求标识,发现若干响应超时发生在订单提交之后。数据库中已经存在待支付订单,库存也已占用,只是同步通知迟迟未完成。新的请求携带新键,服务端把它们识别成新的购买意图,因此创建了额外订单。这不是幂等检查「失效」,而是客户端与服务端对同一购买意图的标识约定没有被遵守。

处置先从限制影响开始:关闭创建订单路径上的同步通知等待,按数据库可承受能力限制入口并发,停止客户端自动生成新键重试,并提供原操作查询入口。对已经生成的额外订单,先核对支付与履约状态,再按业务规则处理。不能仅按同用户、同商品、短时间重复就直接删除,因为用户也可能确实进行了多次购买。

恢复通知时也遇到了代价。历史订单里没有持久化待发送事件,无法仅靠重启消费者补齐全部通知。团队依据订单记录生成受控补发清单,并记录补发标识,避免恢复脚本重复触发。这个问题说明「把通知改异步」还不完整:需要保证待办工作可靠保存,建立发送结果与去重记录,才能支持中断后的恢复。

长期修复把订单和待发送事件放进本地事务,后台发布事件,通知消费者按事件标识去重。客户端为同一购买意图稳定复用幂等键,服务端增加键与请求摘要的冲突校验,并完善处理中状态查询。数据库连接池与入口并发联合调整,防止扩容后总连接数失控。每项改动都对应已经观察到的失败路径,而不是追求组件齐全。

故障后的验证包含正常峰值、通知长时间变慢、响应丢失和重复事件等场景。观察点不仅有 p95 与错误率,还包括每个购买意图的订单数量、库存占用数量、待通知任务年龄和最终通知结果。通知恢复后队列能否清空,也要看恢复期间的新任务到达量;消费者刚好追平新流量时,旧积压并不会自动减少。

这个案例的取舍是:让订单创建不再等待非必要通知,同时接受通知延迟,并承担持久化事件、消费者和核对任务的维护成本。它没有解决支付渠道的所有不确定性,也没有证明任何规模都无需扩容。有效结论应有边界:在给定流量与数据库能力下,这些改动切断了已知放大路径,并用故障实验验证了主要恢复行为。

9 排查案例:证据链怎么写

排查记录首先写现象、范围和时间窗口。例如「14:02 至 14:12,创建订单接口 p95 升高,移动端部分用户收到超时;涉及新建订单,订单查询仍可用」。这是可观察的症状。不要开头就写「数据库性能不足」,因为那是待验证的解释。范围写得清楚,才能判断哪些信号相关,哪些只是同一时刻发生的背景变化。

接着列出少量可以被推翻的假设:应用计算饱和、数据库连接等待、库存变慢、通知阻塞。为每个假设指定证据来源和反证条件。若 CPU 较低、请求在等待下游,计算饱和的解释就减弱;若数据库写入很快但获取连接耗时增加,就应区分数据库执行慢与连接供给受限。这样排查会逐步缩小范围,避免不停切换猜测。

症状、假设、验证与处置的证据链
症状、假设、验证与处置的证据链

以下片段是教学用的关联记录,时间统一使用 UTC。它们表达同一个请求的关键阶段,不代表真实日志系统的固定格式。观察时应把耗时相对关系与业务标识对应起来,不要只搜索包含 error 的行。

14:02:10.000Z request=r81 key_hash=k7 stage=received
14:02:10.160Z request=r81 order=o42 stage=order_committed
14:02:11.000Z request=r81 stage=client_disconnected
14:02:13.180Z request=r81 order=o42 stage=notification_done
14:02:13.190Z request=r81 stage=response_write_failed

这组记录支持「订单提交之后,客户端在等待通知期间断开」的解释,但不能单独证明用户创建了重复订单。还需要找到后续请求,核对不同键是否对应同一用户发起的重复尝试,再查看订单与支付事实。也不能仅靠客户端断开日志断言用户没有收到任何数据,代理层和客户端记录可能提供额外细节。证据链应指出已知事实和剩余未知。

可以把记录写成「现象—证据—解释—行动—验证」的顺序。现象是客户端超时;证据是订单先提交、通知后完成,以及客户端重试使用新键;解释是同步等待拉长响应,重试又绕开同一购买意图的关联;行动是移除通知等待并修复键规则;验证则是注入通知延迟和响应丢失后,仍只有预期数量的订单与库存占用。

证据应能定位到时间窗口、查询条件、配置版本与样本范围。脱离上下文的截图只能说明某个时刻的值,难以复现判断。记录指标变化时,还要注明是否进行了扩容、限流或发布,因为这些动作会改变观察条件。若多项措施同时实施,恢复与某个措施相关,也不等于已经证明它单独消除了根因,后续可用隔离实验补充验证。

处置验证分为用户体验恢复和业务状态收敛。接口延迟下降说明新请求可能恢复,但历史重复订单、未释放占用与漏通知仍需核对。完成标准应明确:新增错误回到约定范围,待处理状态持续减少,受影响订单有处理结果,人工事项有负责人。不能看到曲线回落就关闭事件,把业务尾部留给客服自行发现。

最后写下决策的适用边界和下一次复审条件。例如促销峰值超过已验证范围、热点商品比例明显变化,或最老任务年龄持续越过目标时,要重新评估限流、消费能力和数据访问方式。证据链不仅用来解释过去,也把架构选择变成可修订的假设。初学者不必写长篇报告,但每个重要结论都应能回答:依据是什么,做了什么,如何知道有效。

放进方案里

在零售订单方案中,用简短段落先写业务承诺:创建接口返回成功,表示订单与幂等结果已经持久化,并已获得有效库存占用凭据;订单处于待支付状态,支付完成以核验后的支付事实为准。通知允许延后,但必须可追踪、可补发。把这些承诺写清楚,评审者才能判断接口、状态和异常处理是否相符。

接着给出目标与假设。例如峰值每秒 300 个创建请求持续 10 分钟,创建接口 p95 目标为 800 毫秒,正常情况下通知在 5 秒内完成。注明测量范围与业务拒绝的统计口径,并说明这些数值来自需求和待验证假设。若容量不足,入口按已验证能力限流,不能靠无限接受任务来维持表面成功率。

描述主要路径与边界:身份和报价校验、库存占用、本地事务保存订单与待发送事件、返回可查询结果、后台发送通知。注明本地事务覆盖哪些记录,库存与支付跨出了哪些边界。对超时保留未知状态,通过稳定业务标识查询或重放;库存占用失败不创建承诺成功的订单,订单保存失败则进入释放与核对流程。

把代价直接写在选择旁边。异步通知减少前台等待,但增加任务积压、重复投递与补发维护;有限重试改善暂时故障体验,但消耗调用预算;入口限流保护数据库,却会拒绝部分新请求。说明备选方案为何暂不采用,例如当前团队与流量下先保留模块化单体,避免过早增加跨服务协调。

最后列出验收证据与复审触发:峰值和依赖变慢实验、同键并发重放、响应丢失、重复消息、库存释放失败,以及支付状态核对。观察接口延迟、系统错误、连接等待和任务年龄,并核对订单、库存、支付事实。指定异常负责人和人工处理入口;当流量、数据分布或团队责任变化时,重新检查原有假设与保护阈值。

10 小结:检查清单

评审订单 API 时,可以逐项检查:

架构权衡的成果,是在已知约束下做出可解释、可验证、可修订的选择。能说明为什么这样承诺、失败后怎样查清、条件变化后何时重评,就具备了继续设计订单系统的基础。

下一课转向 P6 成本建模:把本章验收过的结论带进后续主题,写成可检查的方案约束。

同阶段相关课

AI 答疑

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

配置自己的服务