长链路任务抽象模型
这一篇的目标是把“长任务”从模糊感觉,抽象成一个清晰的系统对象。
一句话概括:长链路任务不是一次函数调用,而是一条拥有状态、进度、结果和失败语义的业务实体。
为什么一定要先做抽象
假设你在做一个 AI 合同审核系统。用户上传一份扫描合同之后,后台要依次完成:
- 文件存储
- OCR 识别
- 版面分析
- 关键信息提取
- Embedding 建索引
- 结果入库
这里每一步都可能:
- 很耗时
- 调用不同服务
- 部分失败
- 需要重试
- 需要把过程反馈给用户
所以它绝对不只是一个 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,而是:
- 创建任务记录
- 返回
taskId - 把
taskId丢进队列
执行阶段
Worker 拿到 taskId 后,再去做:
- 下载文件
- OCR
- 提取字段
- 存结果
查询阶段
前端通过:
GET /tasks/{taskId}
拿到如下数据:
{
"taskId": "task_20260414_001",
"status": "RUNNING",
"progress": 60,
"currentStep": "extract_fields"
}
这时你会发现,整个系统终于“说得清楚”了。
这个抽象为什么重要
把任务抽象清楚后,后面所有能力都能自然长出来:
- 队列:解决削峰和异步执行
- 状态存储:解决查询与可视化
- 重试机制:解决临时失败
- 幂等控制:解决重复提交和重复执行
- 可观测:解决排障与优化
- DAG 编排:解决多步骤依赖与并行
换句话说,这一篇是在给整个系列打地基。
常见技术映射
| 抽象概念 | 常见实现 |
|---|---|
| Queue | Redis / RabbitMQ / Kafka |
| Worker | 后台进程 / 容器 / Serverless |
| State Store | MySQL / Redis |
| Result Store | MySQL / 对象存储 |
| Notify | 轮询 / SSE / WebSocket / 回调 |
这一篇要记住的核心点
- 长链路任务首先是一个有状态的对象,而不是一段阻塞式代码
- 任务系统最基本的三个元素是:状态、队列、执行器
- API 负责创建任务,Worker 负责执行任务,状态存储负责对外讲清楚发生了什么
下一篇开始,单独拆开讲消息队列,看看它为什么是长任务系统最常见的第一块基础设施。