# ⟨主题⟩ · 改造方案 > **背景**:现状问题登记在本文第 0 节(**缺陷唯一归属地**);复盘视角的"承接不住"见《⟨主题⟩-设计思路与取舍.md》第 5 节 > **目标**:⟨把「X」从**一条路的设计**,变成**覆盖全部写接口的事实**⟩ > **边界**:⟨不改省侧协议、不引入分布式事务(改造点全在本服务内);DDL 只列关键字段,完整建表脚本另出⟩ > **与其它方案的关系**:⟨一致性异常台账**复用**《⟨其它⟩-改造方案.md》的 `⟨表名⟩`,**不重复建表**,只新增 `⟨列/取值⟩`⟩ > **依据**:全部改造点均**对应代码中已核实的缺陷,非推测** > **取材**:深挖链路时发现的结构性缺口,按 `value-dig` SKILL 的「机制模式库」(结构性限制 → 可引入机制)比对出改造方向;每个缺陷追问"能否从补漏上升到加机制/重构" > **代码讲解要求(必读)**:本文交付标准是"**照着能讲代码**"——① §1 先给**改造前链路骨架**(整体调用关系,用伪代码写清"谁调谁、每一步做了什么",可标类名/方法名,**不写文件路径与行号**);② **每个改造点**必带「**关键片段**」(伪代码 / SQL,只示形状、不贴源码)。密度基准:`docs/order-改造方案.md` > **产出门槛**:缺口中至少存在一个**②加机制 / ③重构**档的改造点(更好设计、重构、引入中间件级)才立本文档;全是①档补漏(加缓存/加判断/加隔离/改配置)的**不产出**——在设计复盘"重做改什么"小节记录即可 --- ## 0. 现状问题登记(缺陷唯一归属地) > 功能点清单与设计复盘**不承载缺陷表**;深挖中发现的问题全部登记在此,后文逐条消化。 **严重度**:🔴 资金/数据错误 · 🟠 静默失败或重复调用 · 🟡 卫生与可维护性 | # | 缺陷 | 证据 | 后果 | 严重度 | |:-:|---|---|---|:-:| | 1 | ⟨缺陷⟩ | ⟨可核对的事实:行为 / 字段 / 日志⟩ | ⟨后果⟩ | 🔴 | | 2 | ⟨…⟩ | | | 🟠 | | 3 | ⟨…⟩ | | | 🟡 | **归纳**:这些不是孤立的 bug,而是 **⟨N⟩ 处结构性缺口**: 1. **⟨缺口一⟩** —— ⟨…⟩; 2. **⟨缺口二⟩** —— ⟨…⟩; 3. **⟨缺口三⟩** —— ⟨…⟩。 --- ## 1. 总体思路 **改造前链路骨架**(先定位"改的是链路的哪一段"——用伪代码写清谁调谁、每一步做了什么): ```java ⟨入口类#方法⟩() → ⟨A类#方法⟩() // 这一步做了什么(对应缺陷 #1) → ⟨B类#方法⟩() → ⟨C类#方法⟩() // 对应缺陷 #3 ``` 所以改造是做 N 件事,**而不是调参数**: 1. **⟨把一个点的分级 → 铺成一张网⟩** 2. **⟨把「不确定」从日志提升为一等状态⟩** 3. **⟨把状态推进的时机对齐到权威事实⟩** ```mermaid flowchart TB subgraph BEFORE["改造前"] B1["⟨…⟩"] --> B2["⟨…⟩"] B3["⟨…⟩"] --> B4["⟨…⟩"] end subgraph AFTER["改造后"] A1["⟨…⟩"] --> A2["⟨…⟩"] A2 --> A3["⟨…⟩"] A4["⟨…⟩"] --> A5["⟨…⟩"] end ``` --- ## 2. 阶段一 · ⟨阶段主题:先说止血/可见⟩ > 成本最低、收益最大,**优先做**。⟨N⟩ 处改动都只动本服务内部。 ### 2.1 ⟨改造点名⟩(P0,最重要) - **现状**:⟨…⟩(对应第 0 节缺陷 #⟨N⟩)。 - **后果(真金白银)**:⟨…⟩ 这**违反了 ⟨不变量一⟩**。 - **改造**:⟨…⟩ **关键片段 · ⟨片段名⟩**(伪代码 / SQL,只示形状) ```sql ⟨改造后的语句或结构;与改造前的旧写法形成对照⟩ ``` | ⟨对象⟩ | ⟨写/读⟩ | 可逆性 | 处置 | |---|---|---|---| | ⟨接口/动作⟩ | 写 | ⟨拒绝可逆、超时不可逆⟩ | ⟨…⟩ | - **注意**:⟨切换后**必须同步补"超时不可逆"的判断**,否则只是把"归档超时 → 退款"换成"归档超时 → 抛异常 → 同样退款",问题原地不动⟩。 ```mermaid flowchart TD A["⟨入口⟩"] --> B{"⟨判断⟩"} B -->|"改造后"| C{"分类"} B -->|"现状"| Z["⟨旧行为⟩"] C -->|"⟨可逆⟩"| D["⟨处置⟩"] C -->|"⟨不确定⟩"| E["⟨处置⟩"] ``` ### 2.⟨N⟩ ⟨改造点名⟩ - **现状**:⟨…⟩(对应第 0 节缺陷 #⟨N⟩) - **改造**:⟨…⟩ **原则:⟨异常类型就是后果分类的载体,不能在中间被抹平⟩。** - **验收**:⟨人为造一次 ⟨场景⟩,应出现 ⟨可观测结果⟩⟩。 **关键片段 · ⟨片段名⟩** ```java ⟨改造后的分支形状,只示形状⟩ ``` ### 2.⟨N⟩ 阶段一验收标准 | 指标 | 目标 | |---|---| | ⟨…⟩ | ⟨100% 不产生 ⟨错误动作⟩,转 ⟨正确处置⟩⟩ | | ⟨人为造 X⟩ | ⟨出现可观测的 ⟨证据⟩⟩ | --- ## 3. 阶段二 · ⟨阶段主题:再说可靠/对称/一等状态⟩ > 阶段一保证"不再做错",阶段二保证"⟨挂起能被收走⟩"。 ### 3.1 ⟨幂等键改造⟩ - **现状**:⟨没有幂等键 → 可被重复执行;MQ `maxRetry=3` 在异常路径上会重复调用外部⟩(对应缺陷 #⟨N⟩) - **改造**:⟨唯一索引 · 冲突当幂等命中 · 语义:"同一 X + 同一动作 = 只允许一次对外调用"⟩ - **迁移**:⟨先查存量重复,治理后再加索引⟩ **关键片段 · 索引 + 冲突即命中** ```sql ALTER TABLE ⟨表名⟩ ADD UNIQUE KEY ⟨uk名⟩ (⟨列⟩, ⟨列⟩); ``` ```java try { ⟨对外调用⟩; } catch (DuplicateKeyException e) { return; } // 已执行过 → 直接返回,不再调外部 ``` ### 3.2 ⟨挂起态 + 巡检⟩ - **现状**:⟨…⟩(对应缺陷 #⟨N⟩) - **改造**: 1. **⟨新增挂起态⟩**,与"失败""成功"并列,**不占用失败的语义**; 2. **巡检任务**(⟨XXL-Job⟩)按"距离挂起时长"分级:⟨<1h 自动重放一次 / 1–24h 告警 / >24h 人工队列⟩; 3. 重放走**同一套分级链路**(不新开代码路径)。 **关键片段 · ⟨片段名⟩** ```java ⟨挂起态写入 / 巡检取单的形状⟩ ``` ```mermaid flowchart TD A["⟨不确定⟩"] --> B["⟨挂起 + 台账⟩"] B --> C["告警"] B --> D["巡检任务"] D --> E{"挂起时长"} E -->|"⟨短⟩"| F["自动重放"] E -->|"⟨中⟩"| G["升级告警"] E -->|"⟨长⟩"| H["人工队列"] ``` **关键设计:重放不是"再调一次接口",而是"重新走一遍分级"。** 因为对方的状态可能已经变了,重放必须能得出"成功"这个结论。 ### 3.⟨N⟩ ⟨改造点名⟩ | `⟨anomaly_type⟩` 取值 | 触发场景 | 处置方向 | |---|---|---| | `⟨…⟩` | | | - **收益**:⟨两侧的失败**进同一张台账、走同一套告警和处理台**——运维只需要看一个地方。**这是"复用"而不是"新建"的价值。**⟩ ### 3.⟨N⟩ 阶段二验收标准 | 指标 | 目标 | |---|---| | ⟨重复调用外部⟩ | 0 次 | | ⟨挂起单⟩ | 100% 台账可见 + 100% 被巡检捞到 | --- ## 4. 阶段三 · ⟨阶段主题:最后收口卫生问题⟩ | # | 改什么 | 现状证据(缺陷 #⟨N⟩) | 成本 | |:-:|---|---|---| | 1 | ⟨…⟩ | ⟨可核对的事实⟩ | 低 | | 2 | ⟨…⟩ | | 低 | **⟨某条不是文档工作,是防错工作⟩**:⟨文档漂移会让后来人**按错误的图改代码**,成本远高于改文档。⟩ --- ## 5. 实施顺序与成本收益 | 阶段 | 内容 | 成本 | 解决的根问题 | 关键收益 | |:-:|---|---|---|---| | 一 | ⟨…⟩ | 低 | ⟨…⟩ | **不再出错** | | 二 | ⟨…⟩ | 中 | ⟨…⟩ | **不确定可运营** | | 三 | ⟨…⟩ | 低 | 卫生与可维护性 | 可观测与可维护 | **顺序逻辑**:先修**会花错钱**的(阶段一),再修**会重复调用外部系统 / 卡住看不见**的(阶段二),最后才是卫生(阶段三)。 --- ## 6. 迁移与灰度 1. **⟨逐项切⟩**:先切**风险最高、问题最实**的 ⟨X⟩,观察一周无异常再切 ⟨Y⟩;每次切换**只改一个调用点**,便于回滚。 2. **⟨新增状态先"只记录不流转"⟩**:先写台账 + 告警**观察一周**,确认没有误报,再打开自动重放。 3. **⟨索引先查存量⟩**:先排查存量重复;加索引后**观测冲突命中次数**——这个数就是"原设计漏掉的重复调用次数"。 4. **回滚**:全部改造以"**新增状态 + 新增列 + 开关**"方式落地,回滚只需关开关/停调度,**不动存量数据**。 --- ## 7. 改造后:⟨N⟩ 条不变量由谁保证 | 不变量 | 改造前 | 改造后 | |---|---|---| | **⟨不变量一⟩** | ⟨只有一条路成立;某路径会被错退⟩ | ⟨全部写类接口统一分类⟩ | | **⟨不变量二⟩** | ⟨只有一行日志,库里无痕⟩ | ⟨挂起态 + 台账 + 巡检 + 重放⟩ | | **⟨不变量三⟩** | ⟨实际只有两态(超时被降级抹平)⟩ | ⟨异常类型不被中间层改写⟩ | --- ## 附:与《⟨其它⟩-改造方案.md》的关系 | 项 | ⟨其它侧⟩ | ⟨本文⟩ | |---|---|---| | ⟨一致性异常台账⟩ | **建** `⟨表名⟩` | **复用**,只加取值 | | ⟨Outbox⟩ | **建** `⟨表名⟩` | **复用同表**,`⟨biz_type⟩` 区分 | | ⟨幂等⟩ | ⟨…⟩ | ⟨…⟩ | **两边的根因是同一个**:⟨设计了机制,但**没有为"需要人介入"这个信号建自动出口**⟩。所以两套改造共用一套台账与告警,是**结构上的必然,而不是为了省事**。