大型后台管理系统如何把业务操作做可靠(5):可恢复的导入导出任务
一万行商品导入到第 2,500 行时,执行进程退出了。运营关掉过页面,也不知道最后一行是否成功。系统需要回答已经完成了哪些行、哪些还可以处理,以及取消会停在哪里。
先设计可查询的任务资源
上传、提交与执行可以分开。上传完成后,服务端记录文件归属、大小、摘要和版本。提交导入时检查该文件属于当前租户且尚在保留期内,并保存列映射及校验规则版本。任务受理成功后返回稳定地址。
HTTP/1.1 202 Accepted
Location: /jobs/J42
Retry-After: 5
Content-Type: application/json
{"id":"J42","state":"queued"}
在这个示例契约里,状态接口用 200 返回任务资源。执行失败写进任务状态和错误字段。网络错误或查不到任务由 HTTP 错误表达。这样客户端能区分「这次查询失败」和「任务已经失败」。浏览器重新打开时可以从任务列表进入,轮询遵循服务端提示并在隐藏或离开页面时降低频率或暂停。微软异步请求应答模式
分开任务状态和明细状态
任务有排队、运行、请求取消以及终态。终态可按业务分为全部成功、部分完成、失败和取消。逐项处理时,每一行还需要单独的终态。错误结果提供稳定错误码、行号和必要信息,长错误明细分页或下载。
| 观察到的状态 | 可做的动作 |
|---|---|
| 待处理 | 等待执行或请求取消 |
| 已写入 | 查看结果 |
| 明确失败且可重试 | 修正输入或创建重试任务 |
| 提交下游后结果未知 | 查下游结果 |
| 已取消且从未执行 | 根据业务决定是否另建任务 |
「请求取消」可以先落到任务记录,执行端在开始下一批之前检查。并行执行时,还需等待已经取得资格的工作单元完成或确认停下。最后返回成功、失败和取消的实际数量。若任务已经写入 2,500 行,这些行仍保留成功结果。需要撤销的场景另行定义反向操作和受影响对象。
让每行副作用和检查点一起落地
同库导入可以用任务 ID 与稳定行号作为处理键,文件版本固定后,同一行总能再次定位。业务写入和该行结果在一个事务提交,进度从已提交的明细聚合。下面是伪代码。执行端取得任务租约后才进入循环,租约应有期限,接手者更新代次,过期执行端不得继续提交。
processItem(jobId, itemId, workerGeneration):
transaction:
requireCurrentGeneration(jobId, workerGeneration)
item = lockItem(jobId, itemId)
if item.hasFinalResult:
return item.result
if cancellationRequested(jobId):
return saveCancelled(item)
outcome = validateAndApply(item.input)
return saveItemResult(item, outcome)
租约代次校验须与写入同处受保护边界。跨系统写入则按明细操作 ID 查询和去重,不能用本地检查点推断外部副作用。失败行重试要保留原任务和行标识,明确是同一业务意图重放,还是改过数据的重新提交。若两个导入任务可能写同一商品,还要靠商品业务键和写入版本防止覆盖。
导出也需要固定读取规则
任务受理时记下租户、字段、筛选条件和权限策略。若导出要复现用户眼前的数据,后台需要一个可用的快照或版本。若合同允许生成时取数,文件记录实际取数时间与条件。大数据量可分块读取和写入临时文件,全部完成后再将产物标记为可下载。下载入口还要校验当前权限与文件期限。
多租户环境可以对重型任务设并发配额。观察任务积压时分别看等待和执行时间。某租户连续提交大文件时,检查其他租户的短任务是否还能及时开始。
从文件校验到失败行重跑
上传时先限制文件体积、行数、编码和格式。解析阶段记录文件中的原始行号,并把空行、重复主键和字段类型错误分别标出。文件能解析与业务数据能写入是两次判断。例如价格格式合法,但商品所属门店已关闭,后者要在执行阶段再次检查。
导入接口需要约定同一商品重复出现时如何处理。可在预检时拒绝重复业务键,也可规定以最后一行为准并明确展示被覆盖行。若业务依赖顺序,例如先创建分类再导入商品,就不要把全表任意拆成并发批次。可以分阶段执行,先处理分类,再将商品按分类依赖调度。
以一万行文件为例,预检发现 20 行格式错误,剩余 9,980 行进入待执行。某次取消后结果可能如下。数字是示例,预检失败是否计入执行总量需要在接口上明确。
| 分类 | 数量 | 操作人员下一步 |
|---|---|---|
| 预检未通过 | 20 | 修正源文件 |
| 已写入 | 2,500 | 查写入结果 |
| 明确执行失败 | 30 | 下载明细并筛选可重试项 |
| 尚未开始,已取消 | 7,450 | 确认范围后另发任务 |
选择失败项重跑时,服务端创建关联原任务的计划,检查这些项确属可重试且没有在别处完成。若用户更改了商品价格或目标,就要让新请求表达新意图,同时靠业务主键和版本条件决定是否更新。仅重新上传原表,会失去原先成功和失败的边界。
恢复时判断最后一行到底做了什么
第 2,500 行提交前崩溃,持久结果不存在,接手端可再次处理。提交后崩溃,结果与商品写入已经一起提交,接手端直接读取结果。若写入发生在另一个服务而本地结果尚未落盘,接手端先用同一明细操作 ID 查远端。单靠「进度停在 2,499」无法确定第 2,500 行是否完成。
文件生成也可以采用类似原则。每次尝试写不同的临时对象,最终通过带代次检查的任务更新发布下载指针。旧执行端即使晚一步完成,也不能替换新执行端已发布的文件。清理临时对象时排除任务当前引用的产物。
验收任务中断与恢复
在一行事务提交前终止进程,接手后该行可以处理一次。在事务提交后、进度展示更新前终止,接手后已写行应从持久结果重建。再验证取消与最后一批同时发生、下载时已撤权、旧文件已过期这三种情况,分别检查任务状态、真实数据和界面下一步。
总结
任务 ID 保存整次工作的归属和状态,明细 ID 决定每个工作单元如何恢复。只有持久记录能与业务写入对得上,进度、部分成功、取消和失败重试才有可解释的含义。