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

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


前言

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

相关推荐
kyriewen6 小时前
我扒了最近的前端面经——2026年面试不背八股文了,考这5样
前端·面试·ai编程
不简说15 小时前
# JS 代码技巧 vol.7 — 20 个浏览器 API 实战,自带 API 能干的事别自己封装
前端·javascript·面试
程序员清风15 小时前
推荐几个我常听的AI播客!
java·后端·面试
小陈工16 小时前
第7篇:Django框架核心原理与实战深度解析(下)
后端·python·面试
sun༒16 小时前
Java 并发编程精讲,入门
java·开发语言·面试
weedsfly16 小时前
一个电商价格计算案例,带你学会前端开发中的责任链模式
前端·javascript·面试
Irene199117 小时前
证券业数据开发面试资料分享:风控与合规、反洗钱:KYC流程、大额交易报告(≥50万)、可疑交易识别
面试·数据开发·证券业
触底反弹18 小时前
🔥 React 零基础入门(下):Props + State + useEffect 生命周期深度解析
前端·react.js·面试
zzz_236818 小时前
码上面试平台测试报告:功能、后台管理、异常状态与 JMeter 性能验证
jmeter·面试·职场和发展