如何设计幂等且版本化的 REST API
你部署了一个新功能,突然你的 API 收到了来自客户端在超时后重试的重复 POST 请求。你的数据库现在有了两个相同的订单。或者你推出了一项破坏性变更,移动应用因为期望旧的响应格式而崩溃。这是 API 设计中最常见且最痛苦的两个问题:非幂等操作和破坏性变更。本指南将展示如何通过 REST API 中幂等性和版本控制的实用模式来解决这两个问题。
为什么幂等性在 REST API 中很重要
幂等性意味着多次发出相同的请求与发出一次具有相同的效果。HTTP 方法具有定义的幂等语义:GET、HEAD、PUT、DELETE 和 OPTIONS 是幂等的,而 POST 和 PATCH 不是。但现实世界的 API 通常需要接受 POST 来执行创建资源或处理支付等操作。网络故障、客户端超时和重试逻辑可能导致重复。没有幂等性,你可能会面临双重收费、重复记录或状态不一致的风险。
实现幂等 REST API 的技术
1. 对 POST 请求使用幂等键
最常见的方法是让客户端在 Idempotency-Key 头中发送一个唯一键(例如 UUID)。服务器存储该键和操作结果。如果再次收到相同的键,服务器返回存储的结果而不是重新执行操作。
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
实现步骤:
- 客户端为每个逻辑操作生成一个唯一键(UUID v4)。
- 服务器检查该键是否存在于持久化存储中(例如 Redis 或数据库)。
- 如果不存在,处理请求并将键与响应状态和正文一起存储。
- 如果存在,返回存储的响应而不重新处理。
设置合理的过期时间(例如 24 小时)以避免存储无限增长。
2. 利用条件请求
对于更新,使用 ETag 和 If-Match 头来防止丢失更新。服务器返回表示当前状态的 ETag。客户端通过 If-Match 将其发回;如果资源已更改,服务器返回 412 Precondition Failed。这使得 PUT 请求对竞态条件是安全的。
PUT /articles/123
If-Match: "abc123"
Content-Type: application/json
{"title": "Updated title"}
3. 为自然幂等性设计 PUT
当客户端可以确定资源 URL 时,优先使用 PUT 而不是 POST。例如,PUT /users/{userId} 自然是幂等的,因为重复调用会覆盖相同的资源。仅当服务器分配 ID 时才使用 POST。
4. 使用唯一约束处理重复检测
将幂等键与业务键(例如订单号)上的数据库唯一约束结合使用。如果重复漏过,数据库会拒绝它,你可以返回带有明确消息的 409 Conflict。
REST API 的版本控制策略
API 会演进。新字段、更改的行为或删除的端点可能会破坏现有客户端。版本控制让你在不中断它们的情况下引入更改。有几种常见策略:
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URI 路径 | /v1/users |
明确、易于路由、缓存友好 | URL 会变化,可能杂乱 |
| 查询参数 | /users?version=1 |
简单、可选 | 容易遗漏,不符合 REST 风格 |
| 自定义头 | Accept-Version: v1 |
保持 URL 简洁 | 在浏览器中测试较难 |
| 媒体类型 | Accept: application/vnd.api.v1+json |
内容协商,纯 REST | 复杂,不太常见 |
对于大多数团队来说,URI 路径版本控制是最务实的选择。它可见、易于文档化,并且与 API 网关和 CDN 配合良好。
如何在不破坏客户端的情况下进行版本控制
- 从第一天起就在路径中以 v1 开始。之后再添加版本控制会更难。
- 在版本内仅做增量更改:新的可选字段、新的端点。永远不要删除或重命名字段。
- 优雅地弃用:宣布生命周期结束,提供迁移指南,并监控旧版本的使用情况。
- 使用 sunset 头:
Sunset: Sat, 31 Dec 2025 23:59:59 GMT来通知客户端。 - 最多维护两个活跃版本以限制维护负担。
综合应用:一个实际示例
考虑一个支付 API。你想要幂等地创建支付并对端点进行版本控制。
POST /v1/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
服务器逻辑:
- 检查
Idempotency-Key是否存在于 Redis 中。 - 如果存在,返回缓存的响应(状态 + 正文)。
- 如果不存在,处理支付,存储键和响应,返回 201 Created。
对于更新,使用带 ETag 的 PUT:
PUT /v1/payments/123
If-Match: "xyz789"
Content-Type: application/json
{"amount": 1500}
如果 ETag 不匹配,返回 412。这可以防止覆盖并发更改。
常见陷阱及如何避免
- 永久存储幂等键:设置 TTL(例如 24 小时)以避免存储膨胀。
- 忽略竞态条件:使用原子操作(例如 Redis SETNX)来检查和设置键。
- 版本控制太晚:从一开始就添加 /v1。
- 次要版本中的破坏性更改:将任何改变响应结构的更改视为主要版本。
- 未文档化幂等性:明确说明哪些端点支持 Idempotency-Key。
常见问题
什么是幂等 REST API?
幂等 REST API 确保多次发出相同的请求产生与发出一次相同的结果。这对于安全处理重试至关重要,特别是对于像 POST 这样的非幂等方法。
哪些 HTTP 方法是幂等的?
GET、HEAD、PUT、DELETE、OPTIONS 和 TRACE 是幂等的。POST 和 PATCH 默认不是幂等的,但你可以使用幂等键等技术使它们幂等。
对 REST API 进行版本控制的最佳方法是什么?
URI 路径版本控制(例如 /v1/users)是最常见和实用的方法。它明确、易于路由,并且与缓存和 API 网关配合良好。其他选项包括查询参数、自定义头和媒体类型,但它们各有取舍。
准备好测试你的 API 的 JSON 响应了吗?使用我们的 JSON Formatter 快速验证和美化打印负载。