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

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


前言

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

相关推荐
Yuzhiyuxia6 小时前
【AI Agent面试系列】AI Agent实战设计
人工智能·面试·职场和发展
时间的拾荒人7 小时前
Redis 哨兵(Sentinel)详解:原理、选举机制与实战部署
redis·面试·架构·sentinel
nice_lcj5207 小时前
MySQL 存储引擎入门:InnoDB 和 MyISAM 到底怎么选
java·数据库·mysql·面试·innodb·存储引擎·myisam
时间的拾荒人20 小时前
Redis 主从复制详解:配置实操 + 原理机制
数据库·redis·面试
nice_lcj5201 天前
LRU 缓存入门:它是什么、怎么用、面试怎么手写
java·数据结构·缓存·面试·lru
聪明的笨猪猪2 天前
企业知识库AI架构选型:落地导向的架构决策与组件清单
经验分享·面试·架构·embedding·数据库架构
stormzhangV3 天前
涨薪 50% 怎么做到的?
人工智能·面试·aigc
聪明的笨猪猪3 天前
Java 工程案例:AI 生成 @Async 异步线程池踩坑
java·经验分享·面试·ai编程
李剑一3 天前
别再抬杠,前端岗已经没了!无论前后端,我们最终都要成“全干”工程师
前端·面试