接口幂等性设计的常见方案对比

举报
yd_273762305 发表于 2026/11/08 04:05:37 2026/11/08
【摘要】 重复下单、重复扣款怎么防?对比五种幂等方案:唯一索引、幂等令牌、状态机乐观锁、去重表、分布式锁,附对比表和选型建议。

「用户点了两次提交,扣了两次款」——这类问题几乎每个做过交易系统的团队都踩过。根因不是网络不稳定,而是接口没有幂等性。这篇文章对比几种主流的幂等实现方案,给出选型建议。

什么是幂等,为什么重要

幂等的定义:同一个请求执行一次和执行多次,对系统状态的影响相同。

HTTP 语义里 GET/PUT/DELETE 天然幂等,POST 不幂等。但实际的业务接口大多是 POST(创建订单、发起支付、提交工单),这些恰恰是最需要幂等的场景。

需要幂等的典型场景:

- 用户重复点击提交按钮

- 客户端超时重试(不知道第一次是否成功)

- 网关/负载均衡层的重试

- 消息队列的重复投递(at-least-once 语义)

- 定时任务重复触发

方案一:唯一索引(最简单、最可靠)

给业务表加一个唯一约束,比如订单表的 (user_id, request_no) 唯一。重复插入时数据库直接报 Duplicate Key,捕获异常后返回「已处理」。

优点:实现成本极低,数据库层面强保证,并发安全。

缺点:只适用于「插入」类操作。更新和删除类操作不适用。

适用:创建订单、创建用户、提交表单这类纯插入场景。**这是首选方案。**

方案二:幂等令牌(Token)

流程分两步:

1. 客户端先请求一个 token(服务端生成 UUID 存入 Redis,有效期 5 分钟)

2. 提交业务请求时带上这个 token,服务端用 DEL 或 SETNX 原子操作校验并消费

// 校验并消费 token
Boolean ok = redis.delete(tokenKey);
if (!ok) {
    throw new BizException("请勿重复提交");
}
// 继续业务逻辑

优点:适用于任意类型的操作(插入/更新/删除);明确区分「首次」和「重复」。

缺点:需要客户端配合改造(多一次请求);token 生命周期管理有成本。

适用:前端表单提交、需要严格防重的场景。

方案三:状态机 + 乐观锁

对于有状态流转的业务(订单:待支付 → 已支付 → 已发货),用状态机约束流转,配合版本号做乐观锁:

UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = ? AND status = 'PENDING' AND version = ?

影响行数为 0 说明状态已被改过(重复请求或并发),直接返回当前状态即可。

优点:天然支持并发;不需要额外的存储。

缺点:只适用于有明确状态流转的业务。

方案四:去重表

单独建一张表记录已处理的请求:

CREATE TABLE idempotent_record (
  idempotent_key VARCHAR(128) PRIMARY KEY,
  biz_type VARCHAR(32),
  result TEXT,
  created_at DATETIME
);

处理前先插入,插入成功才执行业务;插入失败说明重复,直接查历史结果返回。

优点:通用;可以缓存返回结果,保证重复请求得到相同响应。

缺点:多一次写库;需要定期清理历史数据。

适用:跨多个服务的复杂业务,或者需要「返回相同结果」的场景。

方案五:分布式锁

Boolean locked = redis.setIfAbsent(lockKey, "1", 30, SECONDS);
if (!locked) throw new BizException("请求处理中");

优点:实现简单。

缺点:**只能防并发,不能防重复提交**。第一个请求处理完后释放锁,第二个请求照样能进来。所以严格说这不是幂等方案,只是并发控制。

误用警告:很多团队把它当幂等用,结果在「第一次超时但实际成功」的场景下照样重复执行。

方案对比表

方案 适用场景 实现成本 可靠性
唯一索引 插入类 极低 ★★★★★
幂等令牌 任意,需前端配合 中 ★★★★☆
状态机+乐观锁 有状态流转 低 ★★★★★
去重表 通用,需返回相同结果 中 ★★★★☆
分布式锁 仅并发控制 低 ★★(不是幂等)

关键设计点

1. 幂等键怎么选:必须是业务上唯一的标识。常见选择:客户端生成的 requestId、业务流水号、或者 用户ID + 业务类型 + 业务时间戳 的哈希。不要用自增 ID 或随机数——它们每次都不一样,起不到幂等作用。

2. 幂等有效期设多久:不是永久。用户重试通常发生在几分钟内,设 24 小时足够。太长会占存储,太短(比如 1 分钟)挡不住慢重试。

3. 重复请求返回什么:最佳实践是返回和第一次相同的结果,而不是报错。这样客户端不需要区分「首次成功」和「重复但成功」。去重表方案天然支持这点。

4. 失败的情况也要记录吗:要。如果第一次执行失败(比如库存不足),重复请求应该直接返回同样的失败,而不是再试一次。否则就变成「重试」而不是「幂等」了。

一个容易忽略的坑

幂等键的生成位置很关键。如果由服务端生成,客户端超时重试时会生成新的键,幂等就失效了。必须由客户端生成并携带,或者在请求入口(网关层)根据请求特征生成稳定的键。

另外要注意:幂等和重试是两件事。幂等保证「重复请求不产生副作用」,重试是「失败后再试一次」。有幂等保证后,重试才是安全的。

选型建议

实际项目的组合通常是:

- 创建类接口(下单、注册):唯一索引兜底

- 前端表单:幂等令牌防连点

- 状态流转:状态机 + 乐观锁

- 跨服务编排:去重表 + 结果缓存

不要试图用一个方案解决所有场景。按业务特征选择,组合使用才是常态。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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