1 为什么先学「风险识别」
零售系统的订单 API 看起来只是接收商品、数量和地址,再返回订单号。顾客点击“提交订单”后,请求却可能经过入口、订单服务、库存服务、优惠计算、数据库、支付渠道和消息队列。任何环节变慢、断开或返回含糊结果,都可能改变顾客最后看到的结果。风险识别的起点,就是把“接口能调用”继续追问为“在什么条件下,业务承诺可能无法兑现”。
设想促销开始后,顾客提交购买 1 件商品的请求。页面等待 3 秒后提示超时,顾客再次点击。此时可能发生不同的事情:第 1 次请求根本没有到达服务;请求到达了,但订单没有写入;订单已经写入,只是响应在返回途中丢失。页面上的同样 1 个“超时”,对应的业务状态并不相同。如果团队只把风险写成“接口可能超时”,就无法判断是否会重复下单、占用多份库存,或者让客服误导顾客。
识别风险不是预测所有事故,而是让重要的不确定性提前进入讨论。方案评审时发现“客户端重试可能产生重复订单”,修正往往只涉及请求标识、去重约定和查询入口。上线后才发现,团队还要处理退款、库存恢复和顾客投诉。风险越晚暴露,修复越容易越过技术边界,进入财务、运营和客户服务流程。
本课始终使用零售订单 API 作为例子。假设系统支持创建订单、查询订单和发起支付,库存由独立服务管理,订单变化通过消息通知其他服务。我们的任务是识别顾客和业务可能承受的损失,说明损失如何发生,并找到可以验证的证据。这里不要求掌握复杂架构,也不要求给出精确故障概率。读完后,你应能把“这里不太稳”改写成别人能够复核、测试和处理的风险描述。

2 核心概念与边界
风险可以用 1 句完整的话表达:在某种条件下,因为某个薄弱环节,可能发生某种事件,导致某项业务损失。例如:“促销期间库存响应变慢时,订单服务在超时后自动重试扣减,若库存接口无法识别重复请求,可能重复占用库存,导致可售数量偏低。”这句话同时给出条件、原因、事件和后果,读者可以沿着链路验证,而不是只接受“库存存在风险”的判断。
要区分风险、问题和现象。风险是尚未确定发生的损失可能性;问题是已经确认存在的缺陷,例如订单创建没有去重约束;现象是观测到的表现,例如超时率升高。问题可能成为风险的原因,现象可能成为调查风险的入口,但它们不能互相替代。发现超时率升高,不等于已经证明重复扣款;发现去重缺失,也不能直接宣布所有重试都产生了重复订单。
还要区分触发条件与影响范围。促销流量、依赖服务维护、网络抖动、配置变更,都可能是触发条件。受影响的订单数量、持续时长、资金规模和顾客群体,属于影响范围。把它们分开,可以避免“高峰期会出故障”这种无法评估的说法。即使没有历史故障,也可以明确写出假设:“当创建请求达到平时的 5 倍时,库存等待可能占满订单服务的处理能力。”假设需要验证,不能包装成已经发生的事实。
业务承诺是识别风险的参照。订单场景至少涉及几个不同目标:合法请求应得到可解释的结果;同次购买不应因为重试生成多份订单;库存占用应有明确去向;支付结果应能够核对;顾客信息不应被越权读取。这些目标可能分别受到可用性、数据正确性、容量、权限和依赖变化的影响。分类有助于检查遗漏,但分类名称本身没有解释故障机制,不能只填写“安全风险”“性能风险”就结束。
风险识别也有边界。本课不展开攻击技术、复杂概率模型或完整灾难恢复设计。对于入门方案,先说清保护对象、触发条件、传播过程和业务后果,再记录已有措施、剩余疑问与负责角色,就已经很有价值。风险清单不是对系统缺点的指责,也不是越长越好。需要优先呈现的是可能破坏重要承诺、影响范围较大,或证据不足却被默认安全的事项。
3 关键机制(可迁移)
可以从业务目标出发,沿请求路径寻找状态变化,再在边界处提出失败问题。以创建订单为例,先问顾客希望得到什么结果,再标出哪些环节会写入数据、调用远端服务或异步发送消息。读取失败通常影响当前响应,写入失败还可能留下需要处理的中间状态。只列服务名称很容易漏掉这层区别,因此链路图上应写“保存待支付订单”“占用库存”等动作,而不只是画“订单服务”“数据库”。
对每个边界,可以使用相同的提问方式:如果调用没有到达,会怎样;如果到达但失败,会怎样;如果已经成功而响应丢失,会怎样;如果同个动作执行了 2 次,会怎样;如果执行顺序改变,又会怎样。它们不是所有故障的穷举,却能覆盖许多跨服务风险。特别要记住,调用方只看到超时,通常无法断定被调用方有没有完成写入,这种“结果未知”值得单独记录。
风险还会沿依赖传播。库存服务变慢,可能先增加订单请求的等待时间;等待占用处理资源,随后使健康请求也开始排队;客户端重试又带来更多请求,最终扩大故障。这里的关键不是某台机器坏了,而是等待、资源占用和重试形成了相互推动的关系。识别时应关注这种放大过程,并注明现有超时、并发限制或降级措施是否真的能够截断它。
对发现的风险进行排序时,可以先使用影响、发生可能性和证据可信度这几个维度。影响要落到订单、库存、资金或顾客体验;可能性要说明依据是历史记录、测试还是团队假设;可信度用于提醒哪些判断尚待验证。用“高、中、低”也可以,但必须说明含义。不要把 3 个主观分数相乘得到的数字当作精确概率,更不要因为某种故障很少见,就忽略它可能造成的严重损失。
优先级最终要支持行动。同样是创建失败,只影响 1 次可安全重试的查询,与可能重复收费的写入,其处理紧迫性通常不同。如果某项风险缺少证据,可以先安排小范围测试或补充日志;如果已经证实且影响很大,应明确上线前的处理要求。每项记录至少给出负责角色、下一步动作和验证方式。否则清单虽然完整,团队依然不知道由谁把不确定性变成可接受的结论。
4 关键机制续:落到订单链路
现在把方法用于“创建待支付订单”。假设请求先通过身份与参数检查,再占用库存,随后写入订单,最后发送订单创建消息。这个顺序只是本课的示例,不代表所有系统都必须如此实现。真正需要检查的是:每个动作产生什么状态,后续失败时谁知道它已经发生,谁负责恢复或继续推进。没有写清顺序和状态,就很难讨论补偿是否可靠。
库存已占用而订单写入失败,是典型的中间状态风险。若没有释放机制,库存可能长期不可售。若简单地在捕获异常后发送释放请求,还要继续问:释放请求也超时怎么办;进程退出后谁会补做;释放是否会误伤后来成功的订单。入门阶段不必直接实现复杂协调协议,但应明确需要可查询的占用记录、可重试且不会重复释放的操作,以及用于发现长期未完成记录的核对流程。
订单写入成功而响应丢失,则把风险带到客户端重试。可以为同次购买提供稳定的请求标识,并在服务端保存该标识与结果的关联。重复请求到达时,服务端返回已有结果或说明仍在处理。这个性质通常称为幂等:对同个业务动作重复提交,不应反复产生额外业务效果。仅让客户端按钮变灰并不足够,因为网络库、入口或用户重新打开页面都可能再次发出请求。
请求标识也需要约束。谁生成它、覆盖哪种业务动作、保存多久、是否与顾客身份绑定,都应有约定。同个标识带着不同商品或数量再次到达时,服务端应识别冲突,不能悄悄返回无关订单。保存时间若短于客户端可能重试的时间,较晚到达的重试仍可能产生重复。去重结果放在缓存里时,还要识别缓存丢失后的行为,不能只凭“用了缓存”就判断风险已消除。
异步消息带来另组问题。订单已经保存,消息却没有成功交付,仓储或通知服务可能看不到新订单;消息重复交付,下游可能重复处理;消息顺序变化,取消通知可能先于创建通知到达。需要分别说明如何发现漏发、如何处理重复、如何判断当前有效状态。消息发送返回成功,也不等于所有下游都已完成业务处理,风险描述应准确落在实际承诺的边界。
支付环节尤其需要区分订单状态与资金状态。支付请求超时后立刻把订单标为未支付,并不能证明没有扣款;支付通知重复到达,也不应该重复推进出库。对于结果未知的支付,应通过约定的查询、通知和对账流程逐步确认。订单状态图需要容纳“处理中”或“待确认”等有业务含义的状态,并规定最长等待时间及负责处理的角色,让客服能够解释当前情况。

5 动手与命令/控制台直觉
动手练习应在隔离的测试环境进行,使用测试账号、虚拟商品和不会产生真实扣款的支付方式。先准备 1 个库存充足的商品,记录初始库存,再创建订单并查询最终状态。只看到 HTTP 返回成功还不够,还需要核对订单数量、库存占用和消息处理结果。练习目标是建立“请求、状态、证据”之间的对应关系,而不是观察页面是否出现成功提示。
下面用示意地址展示请求形状。X-Request-Id 用于关联本次调用,Idempotency-Key 表示同次购买动作;它们是本课的接口约定,真实系统需要查阅自己的文档。令牌通过环境变量提供,日志中不应输出其原值。商品、顾客身份和接口路径也都只是测试示例。
curl -i --max-time 3 \
-X POST 'https://orders.test.example/orders' \
-H "Authorization: Bearer ${TEST_TOKEN}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: req-demo-01' \
-H 'Idempotency-Key: buy-demo-01' \
--data '{"sku":"sku-demo","quantity":1}'
先发送正常请求并保存订单号,再保持购买标识和请求内容不变进行重试,同时为后续调用使用新的请求关联标识。预期是这些调用都关联到相同的购买结果,库存也只占用 1 份。如果接口把重复请求明确返回为“处理中”,应继续按约定查询,而不是把处理中直接判为失败。还可以修改数量但保留购买标识,观察系统是否给出明确冲突提示。
接下来通过测试环境提供的延迟开关,让库存响应晚于订单服务的等待上限。没有故障注入能力时,可以先用已批准的模拟服务练习。客户端设置等待上限,只能改变客户端何时停止等待,不能证明服务端取消了操作。超时后应查询订单和库存记录,记录“响应超时但订单成功”“响应超时且没有订单”或“仍待确认”等实际结果,不要只保留命令退出码。
控制台上的直觉也类似。先确认时间范围、环境和请求筛选条件,再查看入口请求量、订单服务错误、库存等待与订单写入记录。若测试持续了 2 分钟,却查看整天的平均值,短时异常很容易被稀释。执行前写下预期结果与停止条件;出现意外库存占用时,应先停止继续注入并保存证据,再按测试清理流程恢复,避免后续请求污染判断。
重试次数可以作为讨论取舍的入口。假设有 100 次原始购买请求,每次最多额外重试 2 次,那么在所有尝试都满足重试条件的极端情况下,调用尝试可能达到 300 次。这只是请求次数上界的示意,不代表一定成功更多,也不是生产容量预测。依赖持续过载时,增加重试可能让成功率更低。应该结合可重试错误、总等待预算、间隔和幂等保障共同判断。
6 观测:看哪些字段与信号
风险能否被验证,很大程度取决于是否留出了证据。请求日志应能回答何时发生、发生在哪个环境、由哪个接口处理、耗时多久,以及返回什么结果。还应记录请求关联标识、订单标识和必要的依赖调用结果。用于去重的购买标识可以采用受控的摘要或内部引用记录,避免为了排查方便而扩大敏感信息的暴露范围。顾客姓名、完整地址和支付凭据通常不需要进入普通诊断日志。
同次购买可能包含多次调用,因此“请求数”与“订单数”必须分开。入口每接收 1 次 HTTP 调用,可以计为 1 次请求;同个购买标识的重试可能仍只产生 1 份订单。若看见请求量增加而订单量不变,可能是重试增加,也可能是参数校验拒绝变多。需要结合状态码、错误类别和去重命中情况解释,不能从 2 条曲线直接推断具体原因。
最基础的信号包括请求量、错误比例和延迟分布。平均耗时会掩盖少数很慢的请求,因此还应看较高分位的耗时,例如 P95 表示约 95% 的观测请求不超过该值。选择时间窗口时要保证有足够样本,低流量场景下某个分位值可能不稳定。错误比例也要讲清分母:全部创建调用、通过校验的调用,还是去重后的购买动作,不同口径回答不同问题。
业务信号则帮助判断技术异常是否已经变成损失。可以关注长期待确认的订单数、库存占用超过约定时长的记录数、支付成功但订单未更新的记录数,以及创建后消息迟迟未处理的数量。每个信号都需要时间口径和状态定义。例如“支付未更新”必须等待正常通知处理窗口结束,才适合作为异常候选,否则正常的短暂延迟会产生大量误报。
关联分析时,先把业务结果与技术信号放在相同时间线上。错误比例上升之前是否发生配置变更;库存等待增加是否早于订单排队;重试增长是否随后出现,都比“某条曲线最高”更有解释力。时间上的先后能够支持假设,却不能单独证明因果,还需要调用记录、状态变化或受控测试补充。记录时间最好使用可对齐的时区与精度,并留意不同节点的时钟偏差。
告警需要连接到行动。对于订单长期待确认,告警内容应提供环境、时间范围、数量、可查询的样例标识与处置入口,让值班人员知道先核对什么。阈值应结合业务容忍时间和日常基线制定,不宜机械照搬示例数字。日志与链路记录也可能因为采样或保留期限而不完整;没有查到某条记录时,应先检查证据覆盖范围,再判断业务动作是否没有发生。

7 常见坑与反模式
常见的第 1 个坑,是把组件清单当风险清单。“数据库、库存、支付都有风险”只指出了调查范围,无法指导处理。应继续补足触发条件与业务后果,例如“数据库连接等待超过接口预算时,订单写入结果可能无法及时返回,客户端重试需要查询同次购买结果”。描述越能指向可观察的状态变化,越容易安排测试和明确责任。
第 2 个坑,是把所有异常都交给重试。参数错误、权限拒绝、业务库存不足,通常不会因为立即再试而自然恢复;远端已经成功但响应丢失的写入,则需要先处理重复效果的问题。即使错误适合重试,也要限制次数与总耗时,考虑间隔和同时发起的数量。否则原本只影响库存服务的延迟,可能通过重试扩散到入口、订单服务和数据库。
第 3 个坑,是把“有措施”写成“没有风险”。有备份不等于已验证恢复时间;有告警不等于能发现静默的数据错误;有多个实例也不等于能够承受共享数据库故障。应注明措施覆盖什么条件、最近怎样验证、还剩什么空白。比如购买标识能够拦截正常时间窗口内的重复提交,但标识保存过期后的迟到请求仍需单独讨论。
第 4 个坑,是用测试环境成功替代容量和边界判断。低并发下重复发送 2 次得到相同订单,只能支持某个简单场景下的结论,不能证明并发到达、进程重启和数据保存失败时仍正确。风险记录应把已验证与待验证分开,也不要让初学者误以为必须立即覆盖所有复杂故障。先围绕影响最大的业务承诺选择少量代表性场景,再逐步扩大覆盖。
还有种容易忽视的反模式,是默认由“系统自动恢复”,却没有指定恢复条件和失败后的接手角色。库存占用过期是否会释放,支付晚到是否会改变订单,人工处理是否需要留痕,都应有明确答案。自动恢复本身也可能失败。风险清单应保留升级路径和查询入口,使运营、客服和技术团队能够围绕相同状态协作,避免各自采取互相冲突的动作。
8 案例:订单系统相关故障
下面是用于教学的假设案例。某零售活动开始后,创建订单的超时比例升高,客服同时收到“下单失败但库存减少”的反馈。初始猜测是数据库写入变慢,但时间线显示库存调用耗时先升高,随后订单服务的等待请求增多。部分客户端在超时后立即重试,入口请求量继续增长。这些证据提示存在依赖变慢与重试放大的可能,但仍未解释库存最终去了哪里。
团队抽取 1 组测试顾客的购买记录,将购买标识、请求标识、库存占用标识和订单号关联。结果发现,部分第 1 次请求已完成库存占用,却在订单保存前因等待预算耗尽而退出;释放操作依赖当前请求继续执行,退出后没有可靠记录推动释放。另有部分重试使用了新的购买标识,服务端把它当成新的购买动作处理,造成额外占用。这是 2 条不同机制,应分别记录与处理。
此时可先限制额外重试和并发,缓解新的压力,同时把超时页面改为引导查询购买结果。对已有库存占用,应依据真实订单和占用记录核对后恢复,不能因为创建接口报错就批量释放:其中可能存在已经成功保存、只丢失响应的订单。对于支付相关记录,还必须核对资金状态,防止释放库存后仍继续推进已付款订单。止损动作也需要明确适用范围。
后续修正包括为同次购买保留稳定标识、为库存占用保存可核对记录,并让未完成流程具有可持续推进的处理入口。这里的重点不是承诺某种架构能够消除所有异常,而是把原先隐含的中间状态变为能查询、能解释、能处理的状态。修正验收应同时检查最终订单数、库存占用、顾客看到的结果与恢复耗时,不能只看超时曲线是否下降。
案例结束后,风险记录可以写为:“库存延迟超过创建预算时,已成功占用但未关联订单的记录可能无法及时释放,导致可售库存偏低。测试已复现,影响范围取决于异常持续时间内的占用数量。需补充持续核对与恢复机制,由订单和库存负责角色共同验收。”这比“库存稳定性差”更容易进入排期,也便于未来判断修复是否真正覆盖了原风险。
9 排查案例:证据链怎么写
证据链的作用,是让未参与现场的人也能复核判断。可以从问题陈述开始:“测试环境在指定时间段出现创建请求超时,部分购买仍有库存占用。”这里先描述已经观察到的事实,不提前写根因。然后列出影响口径,例如涉及哪些接口、商品和测试购买动作,哪些数据仍未取得。时间范围、环境、数据来源和筛选条件都应随证据保留,避免不同人查到不同集合。
接着按业务动作整理时间线,而不是堆叠整段日志。例如记录请求进入、库存占用完成、订单写入结果、响应结束和客户端重试。每个关键节点注明对应标识与结果,不能取得的节点明确写为“未知”。如果库存记录只有保存时间,而订单日志来自另一台机器,就应注明时钟偏差的可能影响。跨节点相差很小的时间,不能直接拿来断言严格的执行先后。
然后提出可被推翻的假设。假设“库存慢导致订单等待耗尽”,就寻找库存耗时与等待结束之间的对应记录;假设“重试造成额外占用”,就查同次购买是否生成多个不同占用标识,以及重试时使用的购买标识是否相同。也应检查竞争解释,例如人工测试本来就提交了不同购买、库存恢复任务尚在正常等待窗口内。证据链的质量来自区分解释的能力,不来自日志数量。
结论需要区分事实、推断和待验证事项。事实可以是“该占用记录存在且未关联订单”;推断可以是“退出路径缺少后续释放记录,可能导致占用滞留”;待验证事项可以是“服务重启后该记录是否仍能被恢复流程发现”。若通过受控测试复现并验证修正后不再出现,结论的可信度会提高。即便如此,也要保留测试条件,避免将有限场景扩大成无条件保证。
最后写清行动与验收标准。每项行动应有负责角色、完成条件和验证数据,例如“在库存占用成功后模拟订单保存失败,恢复流程应在约定时间内处理遗留占用,且不得释放已成功订单的库存”。这里的约定时间应由业务需求决定。把查询入口、证据引用和剩余风险附在记录后,后续评审才知道哪些问题已经关闭,哪些问题仍需接受、规避或继续处理。

放进方案里
在订单 API 方案中,风险识别应紧贴业务承诺和调用链。先写出创建、查询与支付各自承诺的结果,再标出库存占用、订单保存、资金确认和消息交付等状态边界。对每个边界说明未到达、执行失败、结果未知和重复执行时的表现,让评审者能够从顾客操作一路追到内部状态。
每项重点风险用固定信息表达:触发条件、薄弱环节、可能事件、业务后果、现有措施、证据来源和剩余疑问。例如库存延迟超过创建预算时,已占用库存可能无法及时关联订单;方案应说明如何发现这类记录、如何核对后恢复,以及由哪个角色处理恢复失败。不要只写“增加重试”“加强监控”等无法验收的动作。
对优先级给出理由。可能造成重复收费、错误出库或长时间库存不可售的事项,应结合影响范围和恢复难度讨论;证据不足的高影响假设,应安排针对性验证。主观等级可以帮助排序,但应保留依据与假设,避免用看似精确的分数掩盖未知条件。暂时接受的风险也要说明接受范围和重新评估的触发条件。
验证部分至少覆盖正常创建、同次购买重复提交、依赖超时后查询、库存占用后的保存失败,以及支付结果延迟确认。逐项写出预期订单数、库存变化、状态转换和证据入口,并明确测试环境及停止条件。将技术指标与业务核对结果同时作为验收依据,才能判断修正是否保护了原先的承诺。
最后把风险清单连接到上线和运行安排:上线前必须解决什么,运行中看哪些信号,异常由谁接手,什么条件下停止继续放量。为每项行动指定负责角色、期限和完成证据;变更调用顺序、重试策略或依赖接口时重新检查相关风险,使这份清单能够随方案持续更新。
10 小结:检查清单
评审零售订单 API 时,可以用下面的清单快速检查是否遗漏了关键问题。
- 是否写清需要保护的订单、库存、资金与顾客信息,以及各自的业务承诺?
- 是否标出远端调用、数据写入和异步消息等状态边界?
- 是否分别讨论未到达、执行失败、结果未知、重复执行和顺序变化?
- 是否用稳定的购买标识关联重试,并约定冲突与保存期限?
- 是否能发现并处理长期未完成的订单、库存占用与支付确认?
- 是否区分技术请求数、购买动作数和最终订单数?
- 是否为重要判断保留证据来源、假设与尚未确认的部分?
- 是否明确负责角色、下一步动作、验收标准和剩余风险?
如果只能记住 1 个练习,就从“创建订单超时了”开始,继续问:哪些状态可能已经改变,如何查证,重复提交会发生什么,最后由谁把结果确认下来。能把这些问题写清,就已经迈出了风险识别的重要一步。
下一课转向 P8 架构决策记录 ADR:把本章验收过的结论带进后续主题,写成可检查的方案约束。