接口幂等性设计的常见方案对比
「用户点了两次提交,扣了两次款」——这类问题几乎每个做过交易系统的团队都踩过。根因不是网络不稳定,而是接口没有幂等性。这篇文章对比几种主流的幂等实现方案,给出选型建议。
什么是幂等,为什么重要
幂等的定义:同一个请求执行一次和执行多次,对系统状态的影响相同。
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. 失败的情况也要记录吗:要。如果第一次执行失败(比如库存不足),重复请求应该直接返回同样的失败,而不是再试一次。否则就变成「重试」而不是「幂等」了。
一个容易忽略的坑
幂等键的生成位置很关键。如果由服务端生成,客户端超时重试时会生成新的键,幂等就失效了。必须由客户端生成并携带,或者在请求入口(网关层)根据请求特征生成稳定的键。
另外要注意:幂等和重试是两件事。幂等保证「重复请求不产生副作用」,重试是「失败后再试一次」。有幂等保证后,重试才是安全的。
选型建议
实际项目的组合通常是:
- 创建类接口(下单、注册):唯一索引兜底
- 前端表单:幂等令牌防连点
- 状态流转:状态机 + 乐观锁
- 跨服务编排:去重表 + 结果缓存
不要试图用一个方案解决所有场景。按业务特征选择,组合使用才是常态。
- 点赞
- 收藏
- 关注作者
评论(0)