LabHub
学习 学习路径 课程

FDE综合实战:仓库收到了三次相同订单

响应消失了,订单却还在

在 LabHub 中继续学习

一句话总结

请求失败不等于业务失败。只有使用同一个业务键重试,并核对响应与账本,才能辨别是否出现重复预留。

流程图: 一句话总结 · 为什么需要这些知识 · 它是如何运作的 · 发送了两次,为什么只预留了一次

为什么需要这些知识

向仓库 API 发送预留请求后,页面显示连接错误。这时重新发送安全吗?如果连接在保存预留之前中断,就必须重发才能建立预留。反过来,如果保存已经完成,只是响应丢失,那么在没有任何保护的情况下重发,就可能产生两条预留。客户端看到的错误消息相似,服务器内部的业务状态却恰好相反。

本次虚拟仓库会真实制造这种差异。ORD-BEFORE 的第一次请求在保存前返回 503。ORD-AFTER 的第一次请求先把预留提交到 SQLite,然后不发送 HTTP 响应就关闭连接。如果只用一个失败标志处理这两种情况,恢复方案就会设计错误。正因如此,你不能只阅读日志,还要让自己编写的程序亲自通过这两种场景。

它是如何运作的

发送 API 是 POST /reservations。JSON 中只包含 order_id、sku 和 quantity。另用 Idempotency-Key 标头标识同一次业务尝试。本课程使用订单编号作为键。API 与客户端共同约定以下三种情况。

请求 仓库的判断 响应
首次出现的键与有效正文 保存新预留 201,预留 ID
相同键与相同正文 返回现有预留 200,现有预留 ID
相同键与变化后的正文 与现有业务冲突 409,不创建新预留

如果每次都生成新键,所有重试都会被视为新业务。如果只在内存集合中保存已处理的键,进程重启后记忆也会消失。本次服务器把键、正文和预留保存在同一个 SQLite 事务中,并使用键的唯一性约束。仅靠应用程序“先查询,不存在再写入”的逻辑,两个并发请求可能都会看到空状态。数据库也必须共同守住最终的重复判定。

如果把整个正文的哈希作为键,会怎样?同一订单的数量一旦改变,哈希也会变化。服务器会把它视为另一个键,并创建新预留。这不是解决冲突,而是绕过冲突检查。如果客户允许修改订单,就必须另行约定修改 API 和版本规则。本练习遇到 409 时会暂缓处理,并保留现有预留。

客户端为每个请求设置 1 秒限制,只对 503 和连接错误最多允许四次 POST 尝试,每次重试之间等待 0.1 秒。409 或契约错误即使等待也不会改变请求含义,因此应立即停止。这些值是为短时教学验证制定的策略。生产环境中,应根据对方限制、延迟分布、整体处理期限和负载另行确定,而不是建议所有服务一律重试四次。

收到响应后,不会立即记录 confirmed。还要通过 GET /orders/<订单编号> 查询业务结果,确认预留恰好只有一条、SKU 与数量正确;如果 POST 返回了 ID,还要确认查询到的 ID 与之相同。若存在三条预留,不能只选第一条并标记成功。如果查询失败,即使曾经收到过一次 ID,也未满足本次契约规定的确认条件,因此要留下 unconfirmed 和 null。

状态名称也要区分。confirmed 表示已经按照约定标准确认结果,unconfirmed 表示尚未确认,rejected 表示请求因 409 或契约错误被拒绝。如果把 unconfirmed 翻译成“没有预留”,下一位负责人可能会使用新键重新发送。没有观察到的事实,就必须明确记录为尚未观察到。

发送了两次,为什么只预留了一次

按时间顺序跟踪 ORD-AFTER。第一次 POST 时,服务器保存键与正文,并创建预留 ID;随后连接关闭,因此客户端没有收到 ID。第二次 POST 使用相同的键和正文,所以不会创建新预留,而是返回现有 ID。最后一次 GET 会确认该订单是否只有一条预留。报告中的 attempts=2,但实际账本中只有一条预留。两个数字不同并不是错误,而是因为分别统计了重试次数和业务效果。

ORD-BEFORE 同样执行两次 POST,但第一次请求时账本仍为空,直到第二次请求才首次创建预留。只看最终预留数量时,两种情况完全相同,因此故障注入测试还必须区分保存时点。另一方面,重试时更换键的错误方案,在 ORD-BEFORE 场景中可能碰巧正常,却会在 ORD-AFTER 场景中创建两条预留。因此,不能仅凭一个场景成功,就断言恢复策略已经通过验证。

在实际工作中会是什么样

重复处理同一文件并非意外事故,而是常见业务流程。传输负责人变更或作业中断时,同一文件会再次进入。因此,验证不能只看首次运行。应在保留同一账本的情况下重启服务器进程,再次发送相同输入,确认不仅预留数量不变,ID 也保持一致。与“无错误退出”相比,检查业务不变量能更直接地回答这个问题。

本练习只涉及单一仓库的一份 SQLite 账本,不解决分布式事务、多个仓库之间的库存一致性,也不处理键过期后的重复传递。把这些限制写入交接文档,同样属于实现工作的一部分。

接下来要确认什么

下一次测验将分别判断响应丢失、进程重启和内容变化这三种不同情况。实现时也把错误类型、实际尝试次数和业务结果设为独立变量,就能更容易说明哪些事实已经知道,哪些仍然未知。