AI 辅助生成数据修复脚本前,先做回滚清单:一次 UPDATE 漏写范围条件的防线设计
线上系统里,经常会出现一种“不算故障、但必须处理”的数据问题。
例如任务调度模块在一次发布中断后,少量执行记录停留在RUNNING状态。它们实际上已经不再运行,却也没有进入可重试或失败状态,后续流程因此一直被卡住。
这时最容易想到的修复方式,是跑一条更新语句:
UPDATEtask_executionSETstatus='RETRYABLE',updated_at=NOW()WHEREstatus='RUNNING';从语法上看,它没有问题。
从业务意图上看,也像是在把“卡住的任务”恢复到可处理状态。
但真正危险的地方在于:数据库里的RUNNING,不一定都代表“卡住”。
其中可能同时存在:
- 刚刚开始执行、仍然正常的任务;
- 正在被另一台节点处理的任务;
- 因网络抖动暂时没有上报心跳的任务;
- 已经被人工介入、等待确认的任务;
- 历史残留数据;
- 某个特殊分区中不允许自动重试的任务。
如果没有先定义候选范围,这条语句会把所有运行中的任务一起改掉。
表面上,修复完成了。
实际上,正在处理中的任务可能被重复调度;本应等待人工确认的记录被自动推进;原本可追踪的异常,也被一次粗粒度更新覆盖掉了。
这类事故的根源通常不是 SQL 写错,而是“修复目标”从一开始就没有被建模。
一、最常见的错误:先让 AI 给 SQL,再补边界
很多开发者向 AI 描述问题时,往往是这样一句话:
有些任务长期处于 RUNNING 状态,帮我写一条 SQL 把它们改成可重试。这种输入会自然得到类似结果:
UPDATEtask_executionSETstatus='RETRYABLE',retry_count=retry_count+1WHEREstatus='RUNNING';AI 没有“理解错”。
它只是按你给出的范围完成了一个局部任务。
问题是,输入里没有告诉它:
- “长期”具体是多少分钟、多少小时;
- 是否要排除仍有心跳的记录;
- 是否要限制到某次发布前创建的数据;
- 是否要按租户、分区或任务类型过滤;
- 是否允许并发节点继续更新这些记录;
- 已经被人工标记的异常是否需要跳过;
- 一次最多处理多少条;
- 失败后如何恢复到执行前状态。
批量修复最怕的不是语法错误,而是“语法正确、范围错误”。
因为语法错误通常会立即报错,而范围错误往往会完整执行。
二、不要直接修改目标表,先固化候选集
更稳妥的做法,是先把“准备修复的记录”单独落到候选集里。
例如建立一个批次表:
CREATETABLEtask_repair_candidate(batch_idVARCHAR(64)NOTNULL,task_idBIGINTNOTNULL,expected_statusVARCHAR(32)NOTNULL,expected_versionBIGINTNOTNULL,selected_atDATETIMENOTNULL,PRIMARYKEY(batch_id,task_id));第一步不是更新,而是筛选。
INSERTINTOtask_repair_candidate(batch_id,task_id,expected_status,expected_version,selected_at)SELECT:batchId,id,status,version,NOW()FROMtask_executionWHEREstatus='RUNNING'ANDlast_heartbeat_at<:cutoffTimeANDupdated_at<:cutoffTimeANDmanual_hold=0ANDtask_typeIN('SYNC','EXPORT')LIMIT200;这一步的价值很大。
它把“直接改变数据”拆成了两件事:
先确定候选范围 ↓ 再审查候选范围 ↓ 最后执行受约束的更新在真正执行前,团队可以先检查:
SELECTtask_type,COUNT(*)AScandidate_count,MIN(last_heartbeat_at)ASearliest_heartbeat,MAX(last_heartbeat_at)ASlatest_heartbeatFROMtask_repair_candidate cJOINtask_execution tONc.task_id=t.idWHEREc.batch_id=:batchIdGROUPBYtask_type;这比直接看到“更新了 183 条”更有价值。
因为它能让人先判断:这 183 条是不是预期中的那 183 条。
三、候选集正确,不代表执行时仍然安全
即使候选集筛对了,执行阶段仍然可能发生变化。
例如你筛选候选集时,某条任务确实已经 30 分钟没有心跳;但在执行更新前,它可能恢复了,或者被其他节点重新接管。
因此,最终更新必须带上“二次校验条件”。
UPDATEtask_execution tJOINtask_repair_candidate cONc.task_id=t.idSETt.status='RETRYABLE',t.retry_count=t.retry_count+1,t.repair_batch_id=:batchId,t.updated_at=NOW(),t.version=t.version+1WHEREc.batch_id=:batchIdANDt.status=c.expected_statusANDt.version=c.expected_versionANDt.last_heartbeat_at<:cutoffTimeANDt.manual_hold=0;这里的version很重要。
它代表:
候选集建立之后,如果这条记录已经被其他流程修改过,那么本次修复不应该再强行覆盖它。
这本质上是一次乐观锁校验。
如果候选集有 200 条,最后只更新了 187 条,不要急着认为“有 13 条执行失败”。
更合理的理解是:
- 这 13 条在处理期间发生了变化;
- 它们可能已经恢复、被人工处理,或被其他节点接管;
- 需要单独查明,不应自动再跑一次同样的 SQL。
四、错误但常见的处理方式:一次性开事务更新全部数据
另一种常见做法是:
STARTTRANSACTION;UPDATEtask_executionSETstatus='RETRYABLE'WHEREstatus='RUNNING'ANDlast_heartbeat_at<:cutoffTime;COMMIT;它比“没有条件直接更新”好一些,但仍然有几个风险。
| 风险点 | 为什么危险 | 更稳妥的做法 |
|---|---|---|
| 单次范围过大 | 锁时间长,影响正常读写 | 分批处理,限制每批数量 |
| 无审计记录 | 之后无法知道改了哪些数据 | 每批保存候选 ID 与执行结果 |
| 无版本校验 | 覆盖并发修改 | 使用状态和版本双重条件 |
| 无回滚材料 | 改错后难恢复 | 先保存旧状态快照 |
| 无异常分流 | 所有失败都混在一起 | 区分跳过、冲突、执行失败 |
修复前建议额外保存“执行前快照”。
CREATETABLEtask_repair_snapshotASSELECT:batchIdASbatch_id,id,status,retry_count,version,updated_atFROMtask_executionWHEREidIN(SELECTtask_idFROMtask_repair_candidateWHEREbatch_id=:batchId);这张表不是为了鼓励随时回滚。
更重要的是,出现异常后你至少能回答:
- 哪些记录真的被修改过;
- 修改前是什么状态;
- 哪些记录只是进入候选集、最终没有变更;
- 是否需要人工恢复,而不是批量反向更新。
五、让 AI 先审查修复边界,而不是直接生成执行脚本
AI 在这类场景里最有价值的用途,不是替你决定“应该改哪几条数据”。
它更适合帮助你把隐含条件显式列出来。
例如可以这样提问:
你是后端数据修复方案评审助手。 场景: 任务执行表中存在部分 RUNNING 状态记录。 只有超过 30 分钟未上报心跳、未被人工挂起、属于指定任务类型的记录, 才可能允许转为 RETRYABLE。 请不要直接生成全量 UPDATE。 请输出: 1. 候选集筛选条件; 2. 并发修改时需要保留的版本校验; 3. 执行前、执行中、执行后的审计字段; 4. 回滚时需要保留哪些旧值; 5. 哪些记录必须转人工处理; 6. 至少 8 个验证场景; 7. 哪些判断必须由业务负责人确认。这类输入的关键,不是让 AI 给出更复杂的 SQL。
而是先让它参与“边界识别”:
什么可以自动修复 什么只能人工确认 什么必须保留原始证据 什么不能被重试覆盖当 ChatGPT Plus 被用于脚本复核、异常分类、测试补全和执行清单整理时,工具接入准备就不只是会不会写提示词,而是要把输入权限、样本范围、使用周期和异常处理方式一起纳入团队规范。
团队把 AI 工具纳入日常流程时,除了权限与脱敏规范,也应提前确认使用规则、周期和异常处理路径;需要集中核对时可参考:gpt0424com
六、执行完成后,不要只看影响行数
批量修复结束后,最容易犯的错误是只确认一件事:
UPDATE 187 rows这个结果并不能证明修复正确。
至少还要核对以下几类数据:
| 核对项 | 需要确认的问题 |
|---|---|
| 候选数量 | 是否与预审数量一致 |
| 实际更新数量 | 是否存在并发冲突或状态变化 |
| 跳过数量 | 跳过原因是什么,是否合理 |
| 后续重试结果 | 是否真的重新被调度 |
| 异常比例 | 是否出现某一类任务集中失败 |
| 心跳恢复情况 | 是否有仍然卡住的记录 |
| 业务结果 | 下游是否产生重复、遗漏或错序 |
可以补一条核对 SQL:
SELECTstatus,COUNT(*)AStotalFROMtask_executionWHERErepair_batch_id=:batchIdGROUPBYstatus;再结合调度日志、队列积压、任务耗时和失败率做复核。
这里有一个非常重要的边界:
数据状态被改对,不代表业务结果一定已经恢复。
例如状态变成RETRYABLE后,后续调度可能仍然失败;也可能因为外部依赖不可用,任务再次卡住。
所以批量修复不能只停在数据库层面,必须确认它是否真正恢复了后续链路。
七、上线前至少覆盖这些验证场景
对于这类修复方案,建议至少准备下面的验证用例:
| 场景 | 预期结果 |
|---|---|
| 正常卡住记录 | 被纳入候选集并成功更新 |
| 正常运行记录 | 因心跳未超时被排除 |
| 人工挂起记录 | 永远不自动更新 |
| 候选集建立后状态变化 | 因版本不一致被跳过 |
| 候选集建立后心跳恢复 | 因二次条件不满足被跳过 |
| 修复任务中途异常 | 已执行批次可追踪,未执行部分不受影响 |
| 同一批次重复执行 | 不重复增加重试次数 |
| 回滚演练 | 能定位到被实际修改的记录 |
尤其要测试“同一批次重复执行”。
可以在批次表中限制唯一性,例如:
CREATEUNIQUEINDEXuk_task_repair_onceONtask_repair_candidate(batch_id,task_id);并在执行更新时要求:
ANDt.repair_batch_idISNULL这样可以避免同一个批次被误跑两次后,重复增加retry_count。
八、结语
AI 可以很快写出一条能执行的UPDATE。
但数据修复真正困难的部分,从来不在 SQL 语法,而在于:
- 你到底要修哪一批数据;
- 哪些记录正在被其他流程处理;
- 哪些状态不能自动改变;
- 修改后是否还能追溯;
- 失败后怎样恢复;
- 是否已经验证了后续链路真的恢复。
一次数据修复,不应该被当作“改一条 SQL”。
它更像一次小型变更发布:有范围、有审计、有回滚、有验证,也有不能自动化替代的人工判断。
真正安全的修复,不是影响行数看起来正确。
而是每一条被修改的数据,都能解释为什么是它、为什么现在改、改完之后如何确认结果。
