围绕下单场景整理接口幂等与防重复提交方案,包含唯一约束、幂等 token 与状态机控制。

接口防重复提交怎么做?一次幂等设计入门实践
/ Update
8 mins
1527 words
Loading views

接口防重复提交怎么做?一次幂等设计入门实践h1

这篇起因很简单h2

我刚写后端接口时,总觉得“用户点一次按钮,后端就处理一次”是理所当然的。

但后来发现,真实场景里重复请求很常见:

  • 用户连点两次提交按钮
  • 前端超时后自动重试
  • 网络抖动导致客户端重复发起请求

如果后端没有做控制,就可能出现:

  • 重复下单
  • 重复扣款
  • 重复发券

直到一次联调里同一个订单被提交了两次,我才意识到“接口能跑”不等于“业务结果一定对”。

所以我后来开始认真补幂等这块。

什么是幂等h2

我的理解是:

同一个请求,无论执行一次还是多次,最终结果都应该符合预期,不应该产生重复副作用。

比如“查询接口”天然就比较幂等,而“创建订单”“支付回调”这类写操作,就需要额外设计。

一个典型问题场景h2

假设有个下单接口:

@PostMapping("/order")
public Result<Long> createOrder(@RequestBody OrderDTO dto) {
return Result.ok(orderService.createOrder(dto));
}

如果用户因为网络卡顿,连续点了两次“提交订单”,后端可能就创建出两笔完全一样的订单。

问题不在于代码报错,而在于:业务结果错了。

我先说我现在的做法h2

防重复提交常见有 4 类思路:

  1. 前端防抖 / 按钮置灰
  2. 唯一索引做兜底
  3. Token / 幂等号机制
  4. Redis 分布式锁或状态校验

如果是面试场景,我一般会这样回答:

  • 前端先做基础防重复
  • 后端必须做真正兜底
  • 核心写操作通常用“幂等号 + 唯一约束”更稳

方案一:前端按钮置灰h2

这是最表层的一层防护。

比如用户点击“提交订单”后,按钮马上置灰,等接口返回后再恢复。

优点h3

  • 实现简单
  • 用户体验直观

缺点h3

  • 只能防正常用户操作
  • 防不了网络重试、脚本请求、回放请求

所以它只能算“第一层”,不能当最终方案。

方案二:数据库唯一索引兜底h2

如果业务上有天然唯一标识,可以直接加唯一索引。

比如一个用户同一场活动只能领取一次优惠券:

CREATE UNIQUE INDEX uk_user_coupon
ON user_coupon(user_id, coupon_id);

这样即使并发下多次插入,也只有一条能成功。

我的理解h3

数据库唯一约束是最靠谱的兜底手段之一,因为它最终守住了数据层。

适用场景h3

  • 领取优惠券
  • 用户签到
  • 用户报名
  • 某种“同一个人只能成功一次”的业务

方案三:幂等 Token / 幂等号h2

这个方案在写操作里很常见。

思路h3

  1. 客户端发起请求前,先拿一个唯一幂等号
  2. 提交时带上这个幂等号
  3. 服务端判断这个幂等号是否已处理过
  4. 如果处理过,直接返回之前结果或拒绝重复处理

示例流程h3

客户端先获取 idempotentToken
-> 提交订单时携带 token
-> 服务端校验 token 是否已消费
-> 未消费:执行业务并标记已消费
-> 已消费:拒绝重复提交

简化实现思路(Redis)h3

String key = "order:token:" + token;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
throw new IllegalArgumentException("请勿重复提交");
}

这里本质上是利用 Redis 的原子性,把“是否第一次提交”这个判断做掉。

优点h3

  • 适合创建类接口
  • 逻辑清晰
  • 和前后端分离场景兼容得很好

注意点h3

  • Token 要有有效期
  • 成功和失败的业务定义要统一
  • 最好配合数据库唯一约束,不要只靠 Redis

方案四:基于业务状态做幂等控制h2

有些场景不一定用 token,而是直接根据业务状态判断。

比如支付回调:

  • 如果订单状态已经是“已支付”
  • 那么重复回调就直接忽略
if (order.getStatus() == OrderStatus.PAID) {
return;
}

这种方式的本质是:同一个业务只能从某状态推进一次。

适合场景h3

  • 支付回调
  • MQ 消费去重
  • 状态机类业务

我现在会用的组合方案h2

如果让我现在设计“下单防重复提交”,我会这么做:

  1. 前端按钮置灰,减少无效重复点击
  2. 请求带幂等 token
  3. 后端 Redis 校验 token 只消费一次
  4. 数据库关键字段加唯一约束兜底

这样就不是单点防护,而是多层保护。

我踩过的坑h2

坑 1:只做前端防抖h3

前端只能防“普通用户重复点击”,防不了重试和并发。

坑 2:只做 Redis,不做数据库兜底h3

如果极端情况下 Redis 逻辑有问题,数据库层没有约束,还是可能写脏数据。

坑 3:把“重复请求”与“幂等请求”混为一谈h3

重复请求不一定安全,只有你设计过后,它才真正幂等。

联调前我会这样自测h2

  • 连续点击两次提交,是否只生成一笔订单
  • 请求超时后重试,是否会重复处理
  • 并发提交下,数据库是否还能守住唯一性
  • 重复请求时,接口返回是否清晰可理解

面试里我会这样描述h2

可以按这个结构讲:

  • 问题:创建类接口可能因为重复点击、网络重试导致重复写入
  • 方案:前端防抖 + 后端幂等 token + 数据库唯一约束
  • 关键点:前端减少重复,后端真正兜底,数据库保证最终一致性
  • 适用场景:下单、支付、发券、回调处理

这篇我的结论h2

接口防重复提交的核心不是“拦请求”,而是:

即使同一个请求来了多次,后端也只能安全地处理一次。

评论