Appearance
异步操作与作业资源
本章导读:视频转码、报表导出、AI 推理……任何"超过 1 秒"或"跨系统"的操作都不该让 HTTP 请求干等。本章给出 REST 处理长任务的完整模式:202 Accepted + 作业资源 + 三种进度获取方式(轮询/SSE/Webhook)——这是 REST 体系对"异步"的官方答案(RFC 9110 §9.3.3 + Google AIP-151 LRO 模式)。
1. 为什么需要专门模式
HTTP 请求-响应模型隐含两个假设:操作秒级完成、结果就在响应里。长任务打破两者:
- 同步等待 → 线程被占死、网关 60s 超时、重试造成任务重复执行;
- 直接返回 200 空跑 → 客户端不知道何时算完。
REST 的解法:把"一次执行"建模为资源。创建作业返回 202,之后所有状态都通过读取作业资源获得——HTTP 的"超时问题"被转化为"轮询一个秒回的资源"。
2. 模式骨架
http
# ① 提交任务(POST 非幂等 → 要求幂等键)
POST /v1/videos HTTP/1.1
Idempotency-Key: upload-20260918-01
Content-Type: application/json
{ "sourceUrl": "https://files.example.com/raw.mp4", "transcode": ["1080p", "720p"] }
# ② 立即受理
HTTP/1.1 202 Accepted
Location: /v1/jobs/501 ← 作业资源位置
Content-Location: /v1/videos/9 ← 被创建资源(占位)的当前表示
Link: </v1/jobs/501>; rel="monitor" ← RFC 8288:监视链接(语义标签)
Retry-After: 10 ← 建议 10 秒后首次查询
# ③ 轮询作业
GET /v1/jobs/501
{ "id": "501", "type": "video.transcode", "status": "running",
"progress": 42, ... }
# ④ 完成后再读业务资源
GET /v1/videos/9
{ "id": "9", "status": "ready", "playlists": [...] }两个资源各司其职:作业(job)= 执行过程的快照;业务资源(video)= 结果。不要把"进度条"塞进业务资源字段。
3. 作业资源 Schema 设计
jsonc
{
"id": "501",
"type": "video.transcode", // 任务类型(枚举,客户端按它解析 results)
"status": "succeeded", // 状态机:queued → running → succeeded | failed | cancelled | partialSuccess
"progress": 100, // 0-100;不可估长时给 null + 心跳字段
"priority": "high", // 可选
"payload": { "videoId": "9" }, // 业务参数回显(便于排障)
"result": { // 仅 succeeded 时存在:与同步端点同构的结果
"created": ["/v1/videos/9/playlists/1080p", "/v1/videos/9/playlists/720p"]
},
"error": { // 仅 failed:RFC 9457 Problem
"type": "https://api.example.com/problems/source-codec-unsupported",
"title": "源视频编码不支持", "status": 422, "detail": "codec=prores"
},
"createdAt": "2026-09-18T08:00:00Z",
"startedAt": "2026-09-18T08:00:03Z",
"finishedAt": "2026-09-18T08:12:41Z",
"expiresAt": "2026-10-18T08:12:41Z", // 作业记录的保留期限
"links": {
"self": "/v1/jobs/501",
"cancel": "/v1/jobs/501:cancel",
"retry": "/v1/jobs/501:retry",
"events": "/v1/jobs/501/events" // SSE 端点
}
}设计纪律:
- 状态机封闭枚举,终态不可逆;非法迁移(对 succeeded 的作业发 cancel)→
409; result与"同步版本端点"的返回同构——客户端完成后一次性读取即可,无需两套解析逻辑;error直接复用 Problem Details——失败详情与同步失败同一形状;- 作业保留期(expiresAt)后
404/410;文档声明——客户端应把结果落自己的库。
4. 进度获取的三种方式
| 方式 | 端点形态 | 优点 | 代价 |
|---|---|---|---|
| 轮询(默认必选) | GET /jobs/{id} + Retry-After | 简单、无状态、防火墙友好 | 延迟 = 轮询间隔;空转流量 |
| SSE 事件流 | GET /jobs/{id}/events(text/event-stream) | 实时、单向推送够用、走 HTTP | 长连接数管理;代理超时 |
| Webhook 回调 | 提交时给 callbackUrl,完成后 POST 通知 | 服务端对服务端最省资源 | 需验签、重试、乱序处理;公网可达要求 |
推荐组合:轮询保底 + SSE 增强。
4.1 轮询的节奏工程
- 指数退避 + 抖动:
1s → 2s → 4s → …封顶 30s;服务器用Retry-After主动指导(比客户端猜强); - 批量场景用条件轮询:
GET /jobs?status=running&updatedAfter=…(一次看一批),或对作业集合开GET /v1/jobs/changes?since=cursor增量流(Delta 同步模式); - 作业响应带强
ETag+ 支持If-None-Match→ 未变化时304,轮询成本降到近乎零。
4.2 SSE 形状
http
GET /v1/jobs/501/events
Accept: text/event-stream
retry: 3000
event: progress
data: {"status":"running","progress":42}
event: progress
data: {"status":"running","progress":77}
event: done
data: {"status":"succeeded","result":{...}}
(服务器关闭连接;客户端用 Last-Event-ID 可恢复)4.3 Webhook 的可靠性契约
json
POST {callbackUrl}
Content-Type: application/json
X-Webhook-Signature: t=1727251200,v1=5257a8… ← HMAC 时间戳+签名(Stripe 式)
Idempotency-Key: job-501.succeeded.1 ← 同一事件可重投,接收方须去重
{ "id": "evt_9", "type": "job.succeeded", "createdAt": "…", "data": { "job": "/v1/jobs/501" } }- 期望接收方
2xx确认;非 2xx/超时 → 指数退避重试(至少 24h),并文档化重试策略; - 通知只给"发生事件 + 资源链接",不搬运完整数据——让接收方回读 API(拉模式纠偏推送丢序);
- 事件目录(
job.failed、article.published…)是对外契约,纳入版本治理。
5. Google AIP-151 参考实现(LRO)
大规模云 API 的成熟范式,值得借鉴其字段命名:
POST /v1/videos:transcode → 200 { "name": "projects/p/locations/l/operations/501",
"done": false, "metadata": { "progress": 0 } }
GET /v1/{operation.name} → done=true 时 { "response": {...} } 或 { "error": {...} }它的三要点——操作资源(operation)+ done 标志 + response/error 同位——即本章模式的标准答案变体。
6. 常见反模式
- 假异步:POST 同步干完 30 秒才返回 200——网关超时、重试风暴;
- 进度写进业务资源:
video.status=processing 42%与video.status=ready混一起——状态语义膨胀、轮询缓存互相污染; - 无界作业表:不设置 expiresAt/归档,一年后
GET /jobs全表扫描; - Webhook 不带签名与幂等键:接收方要么被伪造事件打穿,要么重复消费炸库;
- 轮询风暴不设退避:千客户端 × 100ms 间隔 = 自己 DDoS 自己——用
Retry-After与 304 控频。
7. 本章小结
- 长任务 =
POST→202 + Location: /jobs/{id};结果、错误、进度、取消、重试全部资源化。 - 作业 schema 三要素:状态机枚举、
result与同步端点同构、error用 Problem。 - 进度通道:轮询保底(ETag/304 + Retry-After),SSE 提体验,Webhook 走服务间——可叠加,签名与幂等不可省。
8. 下一步
平台级防护:限流头部与安全规范 → 限流与安全规范