name: 幂等性模式 description: 用于设计幂等API、安全处理重试或防止重复操作。涵盖幂等性键、最多一次语义和重复预防。 allowed-tools: 读取, 全局搜索, 文本搜索
幂等性模式
设计API和系统以安全处理重试而不产生重复副作用的模式。
何时使用此技能
- 设计能安全处理重试的API
- 实现幂等性键
- 防止重复操作
- 构建可靠的支付/订单系统
- 优雅处理网络故障
什么是幂等性?
幂等操作:无论执行多少次,结果相同
f(x) = f(f(x)) = f(f(f(x))) = ...
示例:
- GET /user/123 → 始终返回相同用户(幂等)
- DELETE /user/123 → 用户删除一次,后续调用无操作(幂等)
- POST /orders → 每次创建新订单(非幂等)
为什么幂等性重要
网络现实:
客户端 ──请求──> 服务器
<──响应── (丢失!)
客户端不知道请求是否成功。
应该重试吗?
没有幂等性:
- 重试创建重复订单
- 客户被重复收费
- 库存重复减少
有幂等性:
- 重试返回相同结果
- 无重复副作用
- 安全重试
HTTP方法幂等性
| 方法 | 幂等 | 安全 | 备注 |
|---|---|---|---|
| GET | 是 | 是 | 无副作用 |
| HEAD | 是 | 是 | 无副作用 |
| OPTIONS | 是 | 是 | 无副作用 |
| PUT | 是 | 否 | 替换整个资源 |
| DELETE | 是 | 否 | 删除是幂等的(已删除 = 无操作) |
| POST | 否 | 否 | 创建新资源 |
| PATCH | 可能 | 否 | 取决于实现 |
幂等性键模式
概念
客户端生成唯一键,服务器跟踪处理过的键
请求1:
POST /payments
幂等性键:abc-123
{金额:100}
→ 处理支付,存储结果与键abc-123
请求2(重试):
POST /payments
幂等性键:abc-123
{金额:100}
→ 查找存储的abc-123结果,返回相同响应
→ 无重复支付
实现
幂等性存储模式:
┌──────────────────────────────────────────────────┐
│ 幂等性键 │ 请求哈希 │ 响应 │ TTL │
├──────────────────────────────────────────────────┤
│ abc-123 │ sha256(...) │ {...} │ 24h │
└──────────────────────────────────────────────────┘
流程:
1. 接收带幂等性键的请求
2. 检查键是否存在于存储中
3. 如果存在:
a. 验证请求哈希匹配(相同请求)
b. 返回存储的响应
4. 如果不存在:
a. 处理请求
b. 存储响应与键
c. 返回响应
键生成
客户端生成键(推荐):
- UUID v4:550e8400-e29b-41d4-a716-446655440000
- ULID:01ARZ3NDEKTSV4RRFFQ69G5FAV
- 自定义:{client_id}-{timestamp}-{random}
要求:
- 全局唯一
- 不可预测(防止猜测)
- 客户端控制键
请求指纹
验证重试是相同请求(不只是相同键):
请求哈希 = 哈希(
方法,
路径,
主体,
相关头部
)
如果幂等性键存在但请求哈希不同:
→ 返回422:"幂等性键被用于不同请求"
最多一次 vs 至少一次
最多一次
操作执行0或1次,永不多次。
使用场景:重复比缺失更糟
- 支付处理
- 订单创建
- 资源供应
实现:幂等性键与去重
至少一次
操作执行1次或更多次。
使用场景:缺失比重复更糟
- 事件通知
- 日志摄入
- 分析事件
实现:重试直到确认,下游处理重复
恰好一次(困难)
操作恰好执行1次。
在分布式系统中极难实现。
通常通过:
- 至少一次交付 + 幂等处理
- 分布式事务(2PC)
- Saga模式与补偿
重复检测策略
策略1:幂等性键存储
存储:Redis或数据库
键:幂等性键
值:{
状态:"处理中" | "完成" | "失败",
响应:{...},
创建时间:时间戳,
过期时间:时间戳
}
TTL:通常24-72小时
策略2:自然键去重
使用业务标识符:
- 订单:{customer_id}-{cart_id}-{timestamp}
- 支付:{order_id}-{amount}-{currency}
- 转账:{sender}-{receiver}-{reference}
在处理前检查自然键是否存在。
策略3:数据库约束
CREATE TABLE orders (
id UUID PRIMARY KEY,
幂等性键 VARCHAR(255) UNIQUE,
...
);
如果幂等性键已存在,插入失败。
策略4:乐观锁
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 123 AND version = 5;
如果版本变化,用新版本重试。
防止并发重复更新。
处理处理中的请求
问题:请求A开始,请求B(重试)在A完成前到达
解决方案1:幂等性键锁
- 第一个请求获取锁
- 重试等待或返回"处理中"
解决方案2:状态跟踪
- 立即存储"处理中"状态
- 重试看到"处理中",等待或返回409
处理中响应:
HTTP 409 冲突
{
"错误": "具有此幂等性键的请求仍在处理中",
"重试后": 5
}
不同上下文中的幂等性
支付API
POST /charges
幂等性键:{uuid}
{
"金额": 1000,
"货币": "usd",
"来源": "tok_visa"
}
关键:永不重复收费
存储:幂等性键 → 支付ID, 状态, 响应
TTL:24-48小时
消息队列
生产者:
- 在负载中包含消息ID
- 用相同消息ID重试
消费者:
- 跟踪处理过的消息ID
- 如果已处理则跳过
去重窗口:基于预期重试窗口
数据库操作
插入带幂等性:
INSERT INTO orders (id, 幂等性键, ...)
VALUES (gen_id(), 'abc-123', ...)
ON CONFLICT (幂等性键) DO NOTHING
RETURNING *;
如果冲突,获取现有记录。
事件溯源
事件通过序列自然幂等:
- 事件ID:{aggregate_id}-{sequence_number}
- 如果序列已存在则拒绝
- 重放安全(事件不可变)
最佳实践
键存储
- 使用快速存储(Redis)用于热路径
- 持久化到数据库以增强耐久性
- 设置适当TTL(通常24-72小时)
- 清理过期键
错误处理
如果处理失败:
1. 存储失败响应与键
2. 客户端重试获取相同错误
3. 客户端必须使用新键重试
这防止对错误请求的无限重试循环。
文档
清晰文档:
- 哪些端点需要幂等性键
- 键格式要求
- 存储结果的TTL
- 重复的错误响应
客户端实现
1. 在首次尝试前生成幂等性键
2. 本地存储键直到确认成功
3. 网络失败时用相同键重试
4. 真正新请求生成新键
5. 不同操作间不重用键
常见陷阱
1. 仅存储成功响应
→ 存储失败响应,否则重试创建重复
2. 短TTL
→ 客户端可能在TTL过期后重试,导致重复
3. 不哈希请求主体
→ 不同请求用相同键被不同处理
4. 并发重试的竞态条件
→ 使用锁或原子操作
5. 不处理部分失败
→ 多步操作使用saga或补偿
相关技能
api-design-fundamentals- API设计模式rate-limiting-patterns- 处理重试distributed-transactions- 多步操作