《接口幂等性设计的三种方案与实践》
前言
大家好,我是liyuhh985。
上次面试阿里二面,面试官问了我一个问题:
"你们项目里是怎么处理接口幂等的?"
当时我答得不太好,面试结束后我仔细研究了一下,发现这玩意儿太重要了------几乎所有写接口都会遇到重复提交的问题。
今天把学习成果整理成文章,顺便也帮大家避坑。
什么是幂等性?
先看定义:
幂等性:同一个操作执行多次,结果是一样的。
听起来有点抽象,我举个例子:
| 操作 | 是否幂等 | 原因 |
|---|---|---|
| 查询用户 | ✅ 幂等 | 查 100 次返回结果不变 |
| 扣款 100 元 | ✅ 幂等 | 只扣一次,重复不扣 |
| 支付 100 元 | ❌ 不幂等 | 每调用一次就扣一次钱 |
为什么需要幂等?
在实际项目中,导致接口重复调用的场景太多了:
- 用户手抖:点击提交按钮两次
- 网络超时:前端超时重试
- MQ 消息重复:消息队列重复投递
- 恶意刷单:接口被重复调用
如果不做好幂等处理,轻则数据重复 ,重则资金损失。
三种幂等方案实战
下面结合我的项目经历,分别介绍三种常用的幂等方案。
方案一:Token 防重
核心思路:先获取 Token,提交时验证后删除。
markdown
1. 用户打开页面 → 后端生成 Token,存入 Redis
2. 用户提交请求 → 带上 Token
3. 后端验证:Token 存在?→ 删除 Token → 执行操作
Token 不存在?→ 返回"重复提交"
代码实现:
java
// 1. 获取 Token
@GetMapping("/token")
public Result<String> getToken(Long voucherId) {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("seckill:token:" + userId, token, 5, TimeUnit.MINUTES);
return Result.ok(token);
}
// 2. 提交时验证
@PostMapping("/seckill/{id}")
public Result seckill(@PathVariable Long id, @RequestParam String token) {
// 先删除 Token(原子操作)
Boolean deleted = redisTemplate.delete("seckill:token:" + userId);
if (!deleted) {
return Result.fail("请勿重复提交");
}
// 执行秒杀逻辑...
return Result.ok();
}
适用场景:前端防重复提交、秒杀抢购
方案二:去重表
核心思路:用唯一标识(如订单号)存入去重表,利用数据库唯一键约束防止重复。
sql
-- 建表 SQL
CREATE TABLE idempotent_dedup (
request_id VARCHAR(64) PRIMARY KEY, -- 唯一标识
status INT DEFAULT 0, -- 0=处理中,1=成功
result VARCHAR(256),
created_at TIMESTAMP
);
代码实现:
java
public void pay(String orderId) {
// 尝试插入去重表
try {
dedupMapper.insert(new Dedup(orderId, 0));
} catch (Exception e) {
// 唯一键冲突,说明已处理过
throw new BusinessException("订单已处理");
}
// 执行支付逻辑...
// 标记成功
dedupMapper.updateStatus(orderId, 1);
}
适用场景:后端重试、MQ 消息去重
方案三:状态机
核心思路:用 SQL 的 WHERE 条件锁定当前状态,只有符合预期才能更新。
java
// 只有 status=0(待支付)才能改成 1(已支付)
UpdateWrapper<Order> wrapper = new UpdateWrapper<>();
wrapper.eq("id", orderId)
.eq("status", 0) // 锁定当前状态
.set("status", 1);
int rows = orderMapper.update(null, wrapper);
if (rows == 0) {
throw new BusinessException("订单状态已变化");
}
生成的 SQL:
sql
UPDATE tb_order SET status = 1 WHERE id = 123 AND status = 0;
rows = 1→ 更新成功 ✅rows = 0→ 状态已被修改,拒绝 ❌
适用场景:订单状态流转、支付状态变更
三种方案对比
| 方案 | 存储 | 优点 | 缺点 |
|---|---|---|---|
| Token 防重 | Redis | 速度快,不增加 DB 压力 | 需要两次请求 |
| 去重表 | 数据库 | 可靠,可查历史 | 增加存储开销 |
| 状态机 | 现有表 | 最常用,直接利用 DB 特性 | 需要状态字段配合 |
项目实战
在我的秒杀项目中,我是这样组合使用的:
- 秒杀下单:Token 防重 + 分布式锁
- 订单支付:状态机(只有"待支付"才能改成"已支付")
- 消息消费:去重表(防止 MQ 重复消费)
改造后,接口重复调用的问题基本杜绝,线上再也没有出现过重复下单的 BUG。
面试怎么答?
面试官问这个问题,你可以这样回答:
"我们项目主要用三种方案来保证幂等:
- Token 防重,用于前端防重复提交;
- 去重表,用于 MQ 消息消费防重;
- 状态机,用于订单状态流转,只有符合预期状态才能更新。
具体用哪种方案,要看业务场景。比如支付这种有明确状态流转的,就用状态机;MQ 消费这种需要持久化的,就用去重表。"
总结
- 幂等性是后端开发必备技能
- 三种方案各有适用场景
- 状态机是最常用的,推荐优先考虑
- 组合使用效果更好
参考资料:
- Seata 官方文档
- MyBatis-Plus 乐观锁
EOF