第 17 章:交付一个可以解释失败的订单内核
综合项目的目标是一个小而清楚的订单内核:它能够在并发、重复与失败时保持库存和订单的关系,并能解释尚未确认的状态。范围可以有限,但每项承诺都应有可执行验收。
17.1 先列出业务身份和状态
订单至少保存租户、操作键、请求指纹、商品与数量、处理状态和结果身份。库存变化有明确用途,事件表关联已确认的业务记录。对外查询原操作键时,应能区分不存在、处理中、已完成与待核查。
状态名本身不能提供保证。需要说明哪笔事务建立它、哪些唯一键保护它,以及发生异常时谁有权继续。不要把 HTTP 超时直接写成订单取消,也不要把数据库重复键直接写成新的成功。
17.2 为每项规则选择一个竞争实验
| 规则 | 正常验证 | 失败验证 |
|---|---|---|
| 库存守恒 | 一次订单共同提交 | 多个买家竞争最后一件 |
| 业务去重 | 原键查询得到结果 | 同键并发与不同参数冲突 |
| 完整事务 | 两侧变化共同成功 | 1213 与 1205 分别处理 |
| 未知提交 | 收到确认后完成 | COMMIT OK 后响应丢失 |
| 本地事件 | 订单与 Outbox 同事务 | 回滚后两者均不存在 |
| 运行恢复 | 读取稳定业务状态 | 进程重启、重复执行与核查 |
这张表是综合项目的验收目标,不表示随书十五项测试已经全部覆盖。现有测试提供库存竞争、同键同参数并发、1213/1205、提交确认丢失和本地 Outbox 的数据库事实;同键不同参数的指纹冲突、完整服务恢复与跨进程重启需要你继续实现并补测。现有 orders 教学表没有租户、请求指纹与完整状态字段,不是一个完整上线订单平台。
17.3 把真实接口放在数据库合同之上
写一个服务接口并不困难,困难在于把错误映射成可操作的用户状态。例如库存不足应明确拒绝;重复键需要核对参数并返回原结果;未知提交应允许查询原请求,而不是要求用户反复点击。
返回信息不能泄露其他租户的订单、内部连接或完整 SQL。维护者的诊断渠道与普通用户查询渠道应分别授权。用稳定原因码关联详细审计,比直接把数据库异常字符串返回界面更合适。
17.4 用同一份证据交接
项目应交付表结构、事务边界、错误分类、重试与核查策略、测试、执行计划和真实运行记录。记录 MySQL 版本、配置、驱动版本和镜像摘要,使下一位维护者能够在变更后重新验证。
上线评审时,让另一位开发者仅依据文档回答:哪一笔订单已确认、哪一个事务已经回滚、哪个请求还未知,以及为什么不允许再换一个键重试。答不出来的地方,通常需要补充状态或证据。
17.5 下一步扩展应沿着新边界验证
接入消息系统时验证发布与消费窗口;接入缓存时说明缓存与数据库谁是事实源;增加只读副本时重新检查读一致性;增加多商品订单时验证锁顺序与业务守恒。每次扩展都应增加针对新边界的实验。
综合练习。 实现一个只有下单、查询原操作键与事件认领的最小服务。提交一页设计、完整故障矩阵和实际运行报告。明确哪些测试已经执行,哪些仍是计划,并用业务不变量判断每个失败场景的最终结果。