《接口幂等性设计的三种方案与实践》

《接口幂等性设计的三种方案与实践》


前言

大家好,我是liyuhh985。

上次面试阿里二面,面试官问了我一个问题:

"你们项目里是怎么处理接口幂等的?"

当时我答得不太好,面试结束后我仔细研究了一下,发现这玩意儿太重要了------几乎所有写接口都会遇到重复提交的问题

今天把学习成果整理成文章,顺便也帮大家避坑。


什么是幂等性?

先看定义:

幂等性:同一个操作执行多次,结果是一样的。

听起来有点抽象,我举个例子:

操作 是否幂等 原因
查询用户 ✅ 幂等 查 100 次返回结果不变
扣款 100 元 ✅ 幂等 只扣一次,重复不扣
支付 100 元 ❌ 不幂等 每调用一次就扣一次钱

为什么需要幂等?

在实际项目中,导致接口重复调用的场景太多了:

  1. 用户手抖:点击提交按钮两次
  2. 网络超时:前端超时重试
  3. MQ 消息重复:消息队列重复投递
  4. 恶意刷单:接口被重复调用

如果不做好幂等处理,轻则数据重复 ,重则资金损失


三种幂等方案实战

下面结合我的项目经历,分别介绍三种常用的幂等方案。

方案一: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 特性 需要状态字段配合

项目实战

在我的秒杀项目中,我是这样组合使用的:

  1. 秒杀下单:Token 防重 + 分布式锁
  2. 订单支付:状态机(只有"待支付"才能改成"已支付")
  3. 消息消费:去重表(防止 MQ 重复消费)

改造后,接口重复调用的问题基本杜绝,线上再也没有出现过重复下单的 BUG。


面试怎么答?

面试官问这个问题,你可以这样回答:

"我们项目主要用三种方案来保证幂等:

  1. Token 防重,用于前端防重复提交;
  2. 去重表,用于 MQ 消息消费防重;
  3. 状态机,用于订单状态流转,只有符合预期状态才能更新。

具体用哪种方案,要看业务场景。比如支付这种有明确状态流转的,就用状态机;MQ 消费这种需要持久化的,就用去重表。"


总结

  1. 幂等性是后端开发必备技能
  2. 三种方案各有适用场景
  3. 状态机是最常用的,推荐优先考虑
  4. 组合使用效果更好

参考资料

  • Seata 官方文档
  • MyBatis-Plus 乐观锁

EOF

相关推荐
Ruihong2 小时前
Vue v-html 与 v-text 转 React:VuReact 怎么处理?
vue.js·react.js·面试
小徐不徐说3 小时前
面试C++易错点总结
开发语言·c++·面试·职场和发展·程序设计·工作
一只幸运猫.4 小时前
字节跳动Java大厂面试版
java·开发语言·面试
YummyJacky4 小时前
阿里ai应用开发面试
面试·职场和发展
Ruihong5 小时前
你的 Vue v-for,VuReact 会编译成什么样的 React 代码?
vue.js·react.js·面试
M ? A5 小时前
你的 Vue 路由,VuReact 会编译成什么样的 React 路由?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
WYiQIU5 小时前
宇树科技Web前端岗(AI方向),这不算泄题吧......
前端·vue.js·人工智能·笔记·科技·面试·职场和发展
M ? A6 小时前
你的 Vue 3 响应式状态,VuReact 如何生成 React Hooks 依赖数组?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
鹏程十八少7 小时前
2.2026金三银四 Android Handler 完全指南:28道高频面试题 + 源码解析 + 图解 (一文通关)
android·前端·面试