Skip to content

批量操作设计

本章导读:"帮我一次创建 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 Long

2.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 部分成功怎么表达(核心难题)

三种约定按顺序选:

  1. 全有或全无(事务批):任一条失败 → 整批回滚 → 400/422 + errors[]。语义最简,客户端最好写;适用于强一致场景。
  2. 部分成功:整体 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 不代表全部成功,客户端必须逐条检查。
  1. 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 指导节奏);进阶推送:SSE GET /jobs/501/events 或 Webhook 回调;
  • 取消:DELETE /v1/jobs/501POST :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. 下一步

限流如何做到"客户端可编程、协议有标准"? → 限流与安全规范