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

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


前言

大家好,我是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

相关推荐
怕浪猫5 小时前
2026 年 AI Agent 面试到底考什么?这套题库覆盖了 90% 的高频考点
面试·aigc·ai编程
泡沫_花火8 小时前
大模型开发相关岗位面试资料(含算法)
语言模型·自然语言处理·面试·职场和发展
战族狼魂12 小时前
AI Agent核心能力解析
面试·ai编程
DeepAgent12 小时前
AI Agent 面试篇(05):AI 面试——对着摄像头怎么答
面试·agent
此时不提桶,更待何时15 小时前
06-08-B-RocketMQ面试与生产事故实战
面试·rocketmq
nowcoder12317 小时前
AI考试怎么准备?先分清AI面试和AI能力考核
ai·面试
自强的小白17 小时前
jvm面试(Gc)
服务器·jvm·面试
lcj251120 小时前
【C++】set和map——详细使用说明
开发语言·c++·笔记·面试
艾莉丝努力练剑21 小时前
【AI大模型接入SDK】ChatSDK 集成测试概述
jvm·c++·人工智能·学习·面试·集成测试·sdk