05-状态存储与进度可视化

状态存储与进度可视化

长链路任务系统做到这里,很多初学者会有一个自然想法:

任务不是已经在队列里、在 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 个阶段:

  1. QUEUED
  2. OCR
  3. EXTRACT
  4. PERSIST

那么给前端的响应可以这样设计:

{
	"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

一旦状态变成 SUCCESSFAILED,前端就停止轮询。

为什么进度可视化很重要

它不只是“界面好看”。

更关键的价值是:

  1. 降低用户焦虑:用户知道系统没卡死
  2. 减少重复提交:用户不会一直刷新
  3. 方便客服与运营:知道任务卡在哪一步
  4. 帮助排障:研发能快速定位慢点和失败点

常见错误设计

1. 只有 SUCCESS / FAILED,没有中间态

前端只能一直转圈,没有任何学习价值,也没有排障价值。

2. 进度完全靠前端猜

前端不应该自己推断进度,应该以后端状态为准。

3. 查询接口直接读队列

队列不是状态存储,查出来的信息会很不稳定。

这一篇要记住的核心点

  1. 状态存储是任务系统的“事实来源”,不是可有可无的附属品
  2. 进度展示最好基于步骤或权重,而不是拍脑袋估百分比
  3. 前端统一通过状态接口查询,Worker 只负责回写状态

下一篇进入可靠性设计:幂等、重试、失败补偿为什么是长链路系统的生死线。

github