API 幂等性:三个最容易漏的地方

举报
yd_251777447 发表于 2026/11/06 15:27:48 2026/11/06
【摘要】 接口重试是分布式系统的日常:网络抖动、超时、网关重发。只要客户端会重试,服务端就必须面对"同一个请求来了两次"的问题。 幂等不是一个开关,而是一组设计决策。这篇讲三个最常被漏掉的点。 一、幂等键由谁来生成 最直觉的做法是服务端自己去重,比如"同一个用户 5 秒内提交相同内容算一次"。这个方案看起来很省事,但它有个致命缺陷: 服务端无法区分"用户真的点了两次"和"客户端重试了一次

接口重试是分布式系统的日常:网络抖动、超时、网关重发。只要客户端会重试,服务端就必须面对"同一个请求来了两次"的问题。

幂等不是一个开关,而是一组设计决策。这篇讲三个最常被漏掉的点。

一、幂等键由谁来生成

最直觉的做法是服务端自己去重,比如"同一个用户 5 秒内提交相同内容算一次"。这个方案看起来很省事,但它有个致命缺陷:服务端无法区分"用户真的点了两次"和"客户端重试了一次"。

用户在弱网下点两次,可能真的是想提交两单;而客户端因为超时重发,是同一次意图。两者在服务端看起来一模一样。

正确的做法是由客户端生成幂等键(也叫请求 ID):

POST /api/orders
Idempotency-Key: 7f3a9c21-4b8e-4d2a-9f1c-6e5d8b7a2c40
Content-Type: application/json

{"sku": "A-100", "qty": 2}

服务端拿到这个 key,先去查是否处理过:

  • 没处理过 → 正常处理,把结果和 key 一起存下来
  • 处理过 → 直接返回上次的结果(注意:返回原结果,不是报错)

要点在于:同一个 key 的重试必须返回同一个响应。如果第一次返回 200 + 订单号,第二次重试却返回 409 "重复提交",客户端的重试逻辑就会认为失败,可能再试一次,陷入循环。

async function createOrder(req) {
  const key = req.headers['idempotency-key'];
  if (!key) return res.status(400).json({ error: 'missing idempotency key' });

  const cached = await idempotencyStore.get(key);
  if (cached) return res.status(cached.status).json(cached.body);   // 原样返回

  const result = await doCreateOrder(req.body);
  await idempotencyStore.set(key, { status: 200, body: result }, { ttl: 86400 });
  return res.status(200).json(result);
}

UUID 是最省事的选择,但要注意:同一个业务意图必须复用同一个 key。如果客户端每次重试都生成新 key,那幂等就形同虚设。key 应该在"用户发起这次操作"时生成一次,之后所有重试都用它。

二、幂等键的存储生命周期

第二件容易漏的事:这个 key 要存多久?

存太短:用户隔了一小时因为网络问题重试,key 已过期,重复创建。

存太长:存储成本高,且 key 空间无限增长。

实践中常见的做法是 24 小时,理由是:绝大多数重试发生在几分钟内,24 小时足够覆盖异常场景,同时存储压力可控。

但更重要的是存储要和业务在同一个事务边界内。如果先写业务数据、再写幂等记录,中间进程挂了,就会留下"业务成功但幂等没记录"的状态——下次重试又会创建一遍。

正确顺序是先占位再处理:

-- 1. 先插入幂等记录(唯一索引兜底)
INSERT INTO idempotency (key, status) VALUES (?, 'processing');
-- 如果唯一索引冲突,说明有并发请求,直接返回"处理中"

-- 2. 处理业务
-- 3. 更新幂等记录为完成,写入响应
UPDATE idempotency SET status = 'done', response = ? WHERE key = ?;

用数据库的唯一索引做兜底,比"先查再插"可靠得多——后者在并发下必然漏。

三、幂等键覆盖的范围

第三个坑:只对"创建"做了幂等,忘了"更新"和"删除"。

很多人觉得更新天然幂等("把状态改成 X,改两次结果一样"),但下面这种就不成立:

PATCH /api/orders/123
{"action": "add_balance", "amount": 100}

这是"增量"语义,执行两次就加 200。任何带增量、计数、追加语义的操作,都不是天然幂等的。

判断方法很简单:问"这个请求执行两次,结果和一次一样吗?"

  • SET status = 'paid' → 幂等
  • balance = balance + 100 → 不幂等
  • INSERT INTO logs ... → 不幂等
  • DELETE FROM cart WHERE id = 1 → 幂等(第二次删不到东西,但结果状态一致)

对于不幂等的操作,要么改成幂等语义(用版本号、用绝对值而不是增量),要么同样套用幂等键。

PATCH /api/orders/123
Idempotency-Key: ...
{"balance": 200}     // 绝对值,天然幂等

四、一个常被忽略的细节:并发同 key

两个请求带着同一个 key 同时到达(客户端超时后重试,但第一次其实还在处理中),怎么办?

三种选择:

  1. 返回 409 + "处理中",让客户端稍后重试 —— 简单,但客户端要处理这个状态
  2. 阻塞等待第一个请求的结果,然后返回同样的响应 —— 体验最好,但要控制等待上限
  3. 直接放行 —— 等于没做幂等,不可取

推荐第 1 种,配合 Retry-After 头:

HTTP/1.1 409 Conflict
Retry-After: 1

关键是不要在这种情况下返回 500,否则客户端可能立刻重试,加剧并发。

五、小结

三件事,归纳起来:

幂等键由客户端生成,同一业务意图复用同一个 key,重试返回原结果而不是报错。

存储生命周期要覆盖重试窗口,且与业务在同一事务边界内,用唯一索引兜底并发。

幂等不只针对"创建",任何增量、计数、追加语义的操作都要单独处理。

最后给一个自检清单,评审接口时可以逐条过:

  • 这个接口如果收到两次完全相同的请求,会发生什么?
  • 客户端重试时,带的是同一个 key 吗?
  • key 过期后重试,会不会重复创建?
  • 并发同 key 时,第二个请求返回什么?
  • 增量类操作有没有改成绝对值或版本号?

文中示例为通用 HTTP 语义,具体实现依框架和存储而异。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。