01-长链路任务抽象模型

长链路任务抽象模型

这一篇的目标是把“长任务”从模糊感觉,抽象成一个清晰的系统对象。

一句话概括:长链路任务不是一次函数调用,而是一条拥有状态、进度、结果和失败语义的业务实体。

为什么一定要先做抽象

假设你在做一个 AI 合同审核系统。用户上传一份扫描合同之后,后台要依次完成:

  1. 文件存储
  2. OCR 识别
  3. 版面分析
  4. 关键信息提取
  5. Embedding 建索引
  6. 结果入库

这里每一步都可能:

  • 很耗时
  • 调用不同服务
  • 部分失败
  • 需要重试
  • 需要把过程反馈给用户

所以它绝对不只是一个 process(file) 函数,而是一个贯穿整个系统生命周期的对象。

任务首先是一个“对象”

如果你把任务设计成对象,那它至少要有这些字段:

字段 说明
taskId 任务唯一标识
taskType 任务类型,比如 contract_parse
payload 任务输入参数
status 当前状态
progress 当前进度
attempt 当前第几次执行
result 成功结果
error 失败原因
createdAt / updatedAt 生命周期时间戳

一个典型任务记录可以长这样:

{
  "taskId": "task_20260414_001",
  "taskType": "contract_parse",
  "status": "RUNNING",
  "progress": 60,
  "currentStep": "extract_fields",
  "attempt": 1,
  "payload": {
    "fileUrl": "https://oss.example.com/contracts/a.pdf"
  },
  "result": null,
  "error": null
}

这比“调用一个函数然后等返回值”强得多,因为它可以被查询、被审计、被恢复、被补偿。

任务 = 生命周期状态机

任务最大的特点是:它一定会经历状态迁移

最简版状态机:

stateDiagram-v2
    [*] --> PENDING
    PENDING --> RUNNING
    RUNNING --> SUCCESS
    RUNNING --> FAILED
    RUNNING --> CANCELED

工程里更常见的版本,会把队列、投递、重试也体现出来:

stateDiagram-v2
    [*] --> CREATED : 创建任务
    CREATED --> QUEUED : 写入队列
    QUEUED --> RUNNING : Worker 开始执行
    RUNNING --> SUCCESS : 完成
    RUNNING --> FAILED : 失败
    FAILED --> RETRYING : 满足重试条件
    RETRYING --> QUEUED : 重新入队
    FAILED --> DEAD : 超过最大重试次数
    SUCCESS --> [*]
    DEAD --> [*]

这个状态机的意义不只是“看起来正规”,而是它直接决定了:

  • 前端如何显示
  • worker 如何接着跑
  • 系统何时重试
  • 失败后是人工介入还是自动补偿

执行和触发必须分离

这是学习长链路任务时最重要的一次思维切换。

错误理解:

API 被调用了,所以 API 就应该把任务做完。

正确理解:

API 只负责“创建任务”,真正执行任务的是后台 Worker。

用图表示就是:

flowchart LR
    U[用户 / 前端] --> A[API 服务]
    A --> S[任务存储]
    A --> Q[消息队列]
    Q --> W1[Worker A]
    Q --> W2[Worker B]
    W1 --> S
    W2 --> S

这个拆分背后的原因很现实:

  • API 要快,才能应对高并发提交
  • Worker 要稳,才能处理慢任务、失败重试、并发调度
  • 状态存储要准,才能把“任务是否完成”这件事对外说清楚

一个标准的接口协议

任务系统的 HTTP 协议一般不是“同步返回最终结果”,而是“返回任务句柄”。

sequenceDiagram
    autonumber
    participant C as 客户端
    participant API as 任务 API
    participant Q as 队列
    participant W as Worker
    participant DB as 状态存储

    C->>API: POST /tasks
    API->>DB: 写入 CREATED
    API->>Q: 发布 task_id
    API-->>C: 202 Accepted + task_id
    W->>Q: 消费 task_id
    W->>DB: 更新为 RUNNING
    loop 查询状态
        C->>API: GET /tasks/{id}
        API->>DB: 读取状态
        API-->>C: status / progress / result
    end
    W->>DB: 写入 SUCCESS

这种协议的关键价值是:

  • 提交和执行解耦
  • 用户不需要一直卡在一个请求上
  • 任何时刻都可以查询当前进展

一个贯穿全文的简单案例

假设我们用“合同 OCR 解析”做例子。

提交阶段

前端上传文件后,请求:

POST /tasks/contract-parse

API 不直接开始做 OCR,而是:

  1. 创建任务记录
  2. 返回 taskId
  3. taskId 丢进队列

执行阶段

Worker 拿到 taskId 后,再去做:

  1. 下载文件
  2. OCR
  3. 提取字段
  4. 存结果

查询阶段

前端通过:

GET /tasks/{taskId}

拿到如下数据:

{
  "taskId": "task_20260414_001",
  "status": "RUNNING",
  "progress": 60,
  "currentStep": "extract_fields"
}

这时你会发现,整个系统终于“说得清楚”了。

这个抽象为什么重要

把任务抽象清楚后,后面所有能力都能自然长出来:

  1. 队列:解决削峰和异步执行
  2. 状态存储:解决查询与可视化
  3. 重试机制:解决临时失败
  4. 幂等控制:解决重复提交和重复执行
  5. 可观测:解决排障与优化
  6. DAG 编排:解决多步骤依赖与并行

换句话说,这一篇是在给整个系列打地基。

常见技术映射

抽象概念 常见实现
Queue Redis / RabbitMQ / Kafka
Worker 后台进程 / 容器 / Serverless
State Store MySQL / Redis
Result Store MySQL / 对象存储
Notify 轮询 / SSE / WebSocket / 回调

这一篇要记住的核心点

  1. 长链路任务首先是一个有状态的对象,而不是一段阻塞式代码
  2. 任务系统最基本的三个元素是:状态、队列、执行器
  3. API 负责创建任务,Worker 负责执行任务,状态存储负责对外讲清楚发生了什么

下一篇开始,单独拆开讲消息队列,看看它为什么是长任务系统最常见的第一块基础设施。

github