Appearance
批量操作设计
本章导读:"帮我一次创建 50 条评论""一次删除这 12 个文件"——REST 的方法与 URI 一一对应单个资源,批量操作是规范留白最多的区域。本章给出四种批量模式(循环调用 / 批量端点 / 作业资源 / 条件批量),重点设计"部分成功"的表达方式与幂等策略。
1. 先想清楚:默认方案就是 N 次调用
HTTP/2+ 多路复用下,客户端并发 20 个 POST /comments 的成本远低于直觉。批量端点是性能优化手段,不是义务——先测量再设计。
| 信号 | 方案 |
|---|---|
| 条数 ≤ 几十、可并行、需要逐条错误反馈 | 客户端并发单个请求 + 幂等键 |
| 条数固定中等(≤ 数百)、需"要么全成要么全败" | 同步批量端点(§2) |
| 条数大、耗时不确定 | 异步作业资源(§3) |
| 目标是"符合条件的所有资源" | 条件化批量(§4) |
2. 同步批量端点
2.1 路由设计(两种主流命名)
# 风格 A:Google AIP-137/233 —— 冒号自定义方法
POST /v1/articles:batchCreate
POST /v1/articles:batchUpdate
POST /v1/articles:batchDelete (AIP-235:删除类批量也用 POST 携带 ID 列表)
POST /v1/articles:batchGet (或用 GET ?ids=1,2,3 —— 见下)
# 风格 B:集合资源上的"批任务"子资源(纯名词派)
POST /v1/articles/batches body 里带 operations[]批量读特例(广泛接受为资源查询,不算"动作"):
GET /v1/articles?ids=1,2,3 ← 简单、可缓存、无 body;注意 414 URI Too Long2.2 请求体:每条操作带 clientRef
json
POST /v1/articles:batchCreate
Idempotency-Key: 01HT… ← 整批一个键(重试重放整批结果)
Content-Type: application/json
{
"operations": [
{ "clientRef": "a", "article": { "title": "第一篇", "authorId": "7" } },
{ "clientRef": "b", "article": { "title": "第二篇", "authorId": "7" } }
]
}clientRef 是客户端自造的关联标识——批量响应里条目没有天然顺序保证,必须靠它对齐请求与结果。
2.3 部分成功怎么表达(核心难题)
三种约定按顺序选:
- 全有或全无(事务批):任一条失败 → 整批回滚 →
400/422+errors[]。语义最简,客户端最好写;适用于强一致场景。 - 部分成功:整体
200 OK(或207 Multi-Status——WebDAV 遗产,慎用,多数 HTTP 栈不认识),body 逐条结果:
json
{
"results": [
{ "clientRef": "a", "status": 201, "article": { "id": "43", … },
"headers": { "Location": "/v1/articles/43" } },
{ "clientRef": "b", "status": 422,
"error": { "type": "https://api.example.com/problems/validation-failed",
"title": "作者不存在", "detail": "authorId=7 not found" } }
]
}- 每条内嵌该子操作本应返回的 HTTP 状态码——这是"HTTP-in-HTTP"模式(参考 WebDAV RFC 4918 的 207 Multi-Status 与 JSON:API 的 errors 思路);
- 文档必须写明:顶层 200 不代表全部成功,客户端必须逐条检查。
207风格(每条带独立媒体类型/状态):仅当团队生态确实需要时采用,否则用方案 2 +200。
2.4 批量写的设计纪律
- 每批上限(如 500 条),超出
400;响应给X-Request-Limit提示; - 顺序语义显式声明:并行处理不保证顺序?串行保序但慢?——推荐"结果按 clientRef 对齐,顺序无语义";
- 批内条目校验失败时,其余条目照常(方案 2)或整批拒绝(方案 1)——禁止"静默跳过";
- 整批幂等:重试返回首次执行的结果存档(同 Idempotency-Key 语义),否则"部分成功 + 重试"会造成重复数据。
3. 异步批量 = 作业资源
大批量/长耗时(导入 10 万行、批量发布)唯一正解:
http
POST /v1/articles/import
Content-Type: multipart/form-data (或直接给文件下载 URL 的 JSON)
Idempotency-Key: imp-2026-09-18-001
HTTP/1.1 202 Accepted
Location: /v1/jobs/501
Retry-After: 5作业资源本身是标准资源:
json
GET /v1/jobs/501
{
"id": "501",
"type": "articles.import",
"status": "running", // queued | running | succeeded | partialSuccess | failed
"progress": { "processed": 3200, "total": 10000 },
"results": {
"succeeded": 3105,
"failed": 95,
"report": "/v1/jobs/501/errors?page=1" ← 失败明细子资源(分页!)
},
"createdAt": "2026-09-18T08:00:00Z",
"startedAt": "2026-09-18T08:00:01Z",
"finishedAt": null,
"links": { "self": "/v1/jobs/501", "cancel": "/v1/jobs/501:cancel" }
}要点(作业模式的完整落地见实战:Spring Boot 实现):
- 客户端跟踪方式:轮询
GET /jobs/{id}(配合Retry-After指导节奏);进阶推送:SSEGET /jobs/501/events或 Webhook 回调; - 取消:
DELETE /v1/jobs/501或POST :cancel——语义(尽力中止 vs 已处理回滚)文档化; - 作业可归档:完成后
GET /jobs/{id}保留 30 天后404/410; - 重入队幂等:失败子项的"仅重试失败部分"应提供
POST /v1/jobs/501:retryFailed。
4. 条件化批量:对整个集合说话
"删除 30 天前的所有草稿"——逐条传 ID 不现实。设计成操作请求描述过滤条件:
http
POST /v1/articles:batchDelete
{ "filter": "status eq 'draft' AND createdAt < 2026-08-19T00:00:00Z",
"dryRun": true } ← 先演练:返回将命中的数量与样本
→ 202 + Location: /v1/jobs/502安全阀三件套(必须全上):dryRun 演练、匹配数硬上限(如单次 ≤ 10 万)、审计记录 + If-Match 类前置校验防"条件随时间扩大打击面"。
5. 方案对比总结
| 方案 | 原子性 | 部分成功 | 进度 | 适用 |
|---|---|---|---|---|
| 客户端并发单请求 | ✘(逐条) | ✔(天然) | ✔(逐条) | 默认选择 |
| 同步批量端点(全有全无) | ✔ | ✘ | ✘ | 强一致小批 |
| 同步批量端点(逐条结果) | ✘ | ✔ | ✔ | 中小批、客户端可处理混合结果 |
| 异步作业 | 视实现 | ✔(报告) | ✔(progress) | 大批量、导入导出 |
| 条件化批量 | ✘ | ✔ | ✔ | 运维型清理,需安全阀 |
6. 本章小结
- 批量是"优化"不是"义务":先并发单请求,压不动再上批量端点,量大必走作业资源。
- 部分成功的表达:顶层 200/202,逐条内嵌状态码 + clientRef 对齐 + 禁静默跳过。
- 批量写必须幂等(整批一个 Idempotency-Key),条件化批量必须 dryRun + 上限 + 审计。
7. 下一步
限流如何做到"客户端可编程、协议有标准"? → 限流与安全规范