状态存储与进度可视化
长链路任务系统做到这里,很多初学者会有一个自然想法:
任务不是已经在队列里、在 Worker 里跑了吗?为什么还要专门做状态存储?
答案很简单:因为用户、前端、运营、排障系统都需要一个统一事实来源,知道任务现在到底进行到哪里了。
队列只能告诉你“消息在不在”,Worker 只能告诉你“我刚做了什么”,但真正对外可查询、可展示、可审计的,一定得落在状态存储里。
状态存储到底存什么
最简单的任务表,至少要有这些字段:
| 字段 | 说明 |
|---|---|
task_id |
任务 ID |
status |
当前状态 |
progress |
百分比进度 |
current_step |
当前执行步骤 |
attempt |
重试次数 |
result_ref |
结果引用 |
error_code / error_message |
错误信息 |
created_at / updated_at |
时间信息 |
如果用了 DAG 或多步骤流程,通常还会再配两张表:
- task_step:记录每个步骤的状态
- task_event:记录关键事件日志
可以用图来理解:
flowchart LR
T[task 主表] --> S[task_step 子表]
T --> E[task_event 事件表]
T --> R[result_ref 结果引用]
这个设计的意义是:
- 主表回答“这个任务整体怎么样了”
- 子表回答“具体卡在哪一步”
- 事件表回答“它一路上发生过什么”
为什么状态不能只放在内存里
因为 Worker 进程不是可靠真相来源。
如果状态只在 Worker 内存里:
- 进程重启就丢了
- 多实例之间没法共享
- 前端查不到
- 排障也没依据
所以真正工程化的做法一定是:
Worker 负责更新状态,数据库 / Redis 负责保存状态,对外查询接口负责读取状态。
一个标准查询流程
sequenceDiagram
autonumber
participant C as Client
participant API as Query API
participant DB as Task Store
participant W as Worker
W->>DB: 更新状态 RUNNING / progress=40
C->>API: GET /tasks/{id}
API->>DB: 读取任务状态
DB-->>API: status / progress / current_step
API-->>C: 返回给前端展示
这条链路特别重要,因为它意味着:
- 前端永远不要直接问 Worker
- 前端只问统一状态接口
- Worker 只管回写状态,不关心页面怎么画
进度到底该怎么算
“进度可视化”最容易写得很假。
比如很多系统一上来就显示:
- 10%
- 40%
- 80%
- 100%
用户一看就知道你在猜。
更合理的做法通常有三种。
1. 按步骤计数
如果一个任务有 5 步,每完成一步就加 20%。
优点是简单,缺点是如果每一步耗时差异很大,观感不准。
2. 按权重计数
例如:
- 文件预处理 10%
- OCR 40%
- 字段提取 20%
- 入库 20%
- 通知 10%
这样比平均分更接近真实耗时。
3. 只展示阶段,不展示百分比
在一些不可预测的任务里,显示:
- 已接收
- 排队中
- 正在 OCR
- 正在提取字段
- 已完成
反而比硬凑百分比更诚实。
一个简单案例:合同解析进度条
假设一个任务有 4 个阶段:
QUEUEDOCREXTRACTPERSIST
那么给前端的响应可以这样设计:
{
"taskId": "task_20260414_001",
"status": "RUNNING",
"progress": 65,
"currentStep": "EXTRACT",
"steps": [
{"name": "QUEUED", "status": "SUCCESS"},
{"name": "OCR", "status": "SUCCESS"},
{"name": "EXTRACT", "status": "RUNNING"},
{"name": "PERSIST", "status": "PENDING"}
]
}
前端就可以同时渲染:
- 总体状态标签
- 进度条
- 当前步骤文案
- 每一步的完成情况
进度如何推给前端
最常见的三种方式:
| 方式 | 特点 | 适合场景 |
|---|---|---|
| 轮询 | 最简单、实现成本低 | 中后台、低频更新 |
| SSE | 服务端单向推送,浏览器友好 | 状态流式更新 |
| WebSocket | 双向通信、实时性更强 | 实时协作、复杂互动 |
如果只是普通任务查询,轮询通常已经够用。
例如前端每 2 秒请求一次:
GET /tasks/task_20260414_001
一旦状态变成 SUCCESS 或 FAILED,前端就停止轮询。
为什么进度可视化很重要
它不只是“界面好看”。
更关键的价值是:
- 降低用户焦虑:用户知道系统没卡死
- 减少重复提交:用户不会一直刷新
- 方便客服与运营:知道任务卡在哪一步
- 帮助排障:研发能快速定位慢点和失败点
常见错误设计
1. 只有 SUCCESS / FAILED,没有中间态
前端只能一直转圈,没有任何学习价值,也没有排障价值。
2. 进度完全靠前端猜
前端不应该自己推断进度,应该以后端状态为准。
3. 查询接口直接读队列
队列不是状态存储,查出来的信息会很不稳定。
这一篇要记住的核心点
- 状态存储是任务系统的“事实来源”,不是可有可无的附属品
- 进度展示最好基于步骤或权重,而不是拍脑袋估百分比
- 前端统一通过状态接口查询,Worker 只负责回写状态
下一篇进入可靠性设计:幂等、重试、失败补偿为什么是长链路系统的生死线。