第 18 / 22 篇

第 17 章:交付一个可以解释失败的订单内核

综合项目的目标是一个小而清楚的订单内核:它能够在并发、重复与失败时保持库存和订单的关系,并能解释尚未确认的状态。范围可以有限,但每项承诺都应有可执行验收。

17.1 先列出业务身份和状态

订单至少保存租户、操作键、请求指纹、商品与数量、处理状态和结果身份。库存变化有明确用途,事件表关联已确认的业务记录。对外查询原操作键时,应能区分不存在、处理中、已完成与待核查。

状态名本身不能提供保证。需要说明哪笔事务建立它、哪些唯一键保护它,以及发生异常时谁有权继续。不要把 HTTP 超时直接写成订单取消,也不要把数据库重复键直接写成新的成功。

17.2 为每项规则选择一个竞争实验

规则 正常验证 失败验证
库存守恒 一次订单共同提交 多个买家竞争最后一件
业务去重 原键查询得到结果 同键并发与不同参数冲突
完整事务 两侧变化共同成功 1213 与 1205 分别处理
未知提交 收到确认后完成 COMMIT OK 后响应丢失
本地事件 订单与 Outbox 同事务 回滚后两者均不存在
运行恢复 读取稳定业务状态 进程重启、重复执行与核查

这张表是综合项目的验收目标,不表示随书十五项测试已经全部覆盖。现有测试提供库存竞争、同键同参数并发、1213/1205、提交确认丢失和本地 Outbox 的数据库事实;同键不同参数的指纹冲突、完整服务恢复与跨进程重启需要你继续实现并补测。现有 orders 教学表没有租户、请求指纹与完整状态字段,不是一个完整上线订单平台。

17.3 把真实接口放在数据库合同之上

写一个服务接口并不困难,困难在于把错误映射成可操作的用户状态。例如库存不足应明确拒绝;重复键需要核对参数并返回原结果;未知提交应允许查询原请求,而不是要求用户反复点击。

返回信息不能泄露其他租户的订单、内部连接或完整 SQL。维护者的诊断渠道与普通用户查询渠道应分别授权。用稳定原因码关联详细审计,比直接把数据库异常字符串返回界面更合适。

17.4 用同一份证据交接

项目应交付表结构、事务边界、错误分类、重试与核查策略、测试、执行计划和真实运行记录。记录 MySQL 版本、配置、驱动版本和镜像摘要,使下一位维护者能够在变更后重新验证。

上线评审时,让另一位开发者仅依据文档回答:哪一笔订单已确认、哪一个事务已经回滚、哪个请求还未知,以及为什么不允许再换一个键重试。答不出来的地方,通常需要补充状态或证据。

17.5 下一步扩展应沿着新边界验证

接入消息系统时验证发布与消费窗口;接入缓存时说明缓存与数据库谁是事实源;增加只读副本时重新检查读一致性;增加多商品订单时验证锁顺序与业务守恒。每次扩展都应增加针对新边界的实验。

综合练习。 实现一个只有下单、查询原操作键与事件认领的最小服务。提交一页设计、完整故障矩阵和实际运行报告。明确哪些测试已经执行,哪些仍是计划,并用业务不变量判断每个失败场景的最终结果。