幂等性模式Skill idempotency-patterns

此技能用于设计幂等API、安全处理重试、防止重复操作,涵盖幂等性键、最多一次语义和重复预防等模式,适用于构建可靠的支付系统、订单系统,关键词包括幂等性、API设计、重试处理、重复预防、网络故障处理、系统可靠性。

架构设计 0 次安装 0 次浏览 更新于 3/11/2026

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 - 多步操作