关于 idempotency
面向编码 Agent 的后端幂等性实践技能,介绍如何使用幂等键保护关键变更,使请求重试不会重复产生副作用。
主要功能
- 01幂等键模式
- 02键的生成与存储指导
- 03重复风险识别
- 04重试时返回缓存响应
适用场景
- 01防止支付端点重复扣款
- 02避免重复创建订单或账户
- 03检查依赖前端防重的 API
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill idempotency安装后在 Claude Code 中输入/idempotency确认已加载。
详细介绍
幂等性
概述
关键操作必须支持安全重试。请使用幂等键。
网络会故障。客户端会重试。用户会重复点击。如果没有幂等性,重试就会导致重复扣款、重复创建订单或数据损坏。
适用场景
- 支付处理端点
- 创建订单
- 任何不应重复执行的操作
- 被要求“相信前端”能防止重复操作
铁律
NEVER rely on frontend to prevent duplicate requests.
没有例外:
- 不能因为“前端会禁用按钮”
- 不能因为“我们会显示加载状态”
- 不能因为“这种情况很少发生”
- 不能因为“用户不会重复点击”
检测:重复风险信号
如果变更操作没有防重复保护,请立即停止:
// ❌ VIOLATION: No idempotency protection
app.post('/payments', async (req, res) => {
const { userId, amount, cardToken } = req.body;
// If this request retries, user gets charged twice!
const payment = await stripeCharge(amount, cardToken);
await db.payments.create({ userId, amount, stripeId: payment.id });
res.json({ success: true });
});
可能出现的问题:
- 网络超时 → 客户端重试 → 重复扣款
- 用户重复点击 → 两个请求 → 重复扣款
- 移动应用的重试逻辑 → 多个请求
正确模式:幂等键
// ✅ CORRECT: Idempotency key protection
app.post('/payments', async (req, res) => {
// Require idempotency key
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({
error: 'Idempotency-Key header is required'
});
}
const { userId, amount, cardToken } = validated(req.body);
// Check for existing request with this key
const existing = await db.idempotencyKeys.findOne({
where: { key: idempotencyKey, userId }
});
if (existing) {
// Return cached response
return res.status(existing.statusCode).json(existing.response);
}
try {
// Process the payment
const payment = await stripeCharge(amount, cardToken);
await db.payments.create({ userId, amount, stripeId: payment.id });
const response = { success: true, paymentId: payment.id };
// Cache the response
await db.idempotencyKeys.create({
key: idempotencyKey,
userId,
statusCode: 200,
response,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
});
res.json(response);
} catch (error) {
// Cache error responses too (optional, depends on error type)
throw error;
}
});
// Client usage:
// POST /payments
// Headers: { "Idempotency-Key": "user-123-order-456-attempt-1" }
幂等键设计
键生成(客户端)
// Option 1: UUID per request
const key = crypto.randomUUID();
// Option 2: Deterministic (better for retries)
const key = `${userId}-${orderId}-${timestamp}`;
// Option 3: Hash of request content
const key = hash(JSON.stringify({ userId, items, amount }));
键存储(服务端)
interface IdempotencyRecord {
key: string;
userId: string;
statusCode: number;
response: any;
createdAt: Date;
expiresAt: Date; // Clean up old keys
}
哪些操作需要幂等性
| 操作 | 风险 | 解决方案 |
|---|---|---|
| 支付 | 重复扣款 | 幂等键 |
| 创建订单 | 重复订单 | 幂等键 |
| 减少库存 | 扣减过量 | 幂等键 |
| 发送邮件 | 重复邮件 | 幂等键 |
| 创建账户 | 重复账户 | 唯一约束 + 幂等键 |
抵御压力的准则
1. “前端会防止重复操作”
压力:“我们会禁用按钮、显示加载状态”
**回应:**网络会自动重试。JavaScript 可能崩溃。用户手速很快。
**行动:**在后端实现幂等性。前端用户体验不等于保护措施。
2. “这种情况很少发生”
压力:“重复操作只是罕见的边缘情况”
**回应:**罕见情况 × 大量用户 = 许多愤怒的用户。一次重复扣款就足以引发客服噩梦。
**行动:**保护所有关键变更操作。
3. “用户不会重复点击”
压力:“我们的用户很谨慎”
**回应:**用户的网络连接可能很慢。按钮可能很小。受挫时就会不断点击。
**行动:**绝不要依赖用户行为。
4. “数据库有唯一约束”
压力:“重复插入会失败”
**回应:**唯一约束会抛出错误。用户会看到错误。用户体验很糟糕。
**行动:**幂等性会返回相同的成功响应。
危险信号——立即停止并重新考虑
- 支付端点没有幂等性
- “前端会处理重复操作的防护”
- 网络重试会造成副作用
- 用户报告重复扣款
- 不支持
Idempotency-Key标头
以上任何一项都意味着:添加幂等性保护。
快速参考
| 不安全 | 安全 |
|---|---|
| 信任前端 | 要求提供幂等键 |
| 重复请求时报错 | 返回缓存的响应 |
| 假设请求只会发送一次 | 按支持重试的方式设计 |
| POST 始终创建新资源 | POST + 键 = 最多执行一次 |
常见的自我开脱(都不成立)
| 借口 | 事实 |
|---|---|
| “前端会防止” | 网络会重试。用户会重复点击。 |
| “很少发生” | 罕见情况 × 规模 = 许多事故。 |
| “用户很谨慎” | 用户也是人。 |
| “唯一约束” | 约束会抛出错误,而不是返回成功。 |
| “太复杂” | 比处理客服工单简单。 |
总结
所有关键变更操作都必须要求幂等键。
永远不要相信前端保护。按幂等键缓存响应。对重复请求返回相同的响应。定期清理旧键。
评论
支持 Markdown