重复提交怎么拦截?

wen python案例 3

重复提交怎么拦截?8种实战方案详解(附代码与避坑指南)

📖 目录导读

  1. 为什么必须拦截重复提交? – 真实案例与损失分析
  2. 前端拦截方案 – 按钮防抖、置灰、Token机制
  3. 后端拦截方案 – 幂等表、Redis锁、数据库唯一索引
  4. 分布式场景下的终极方案 – 基于Redisson的分布式锁
  5. 高并发下的陷阱 – 锁过期、ABA问题、降级策略
  6. Q&A常见问题 – 用户反复刷新/断网重连怎么办?
  7. 实战代码一键复用 – 拦截器+注解+全局配置

为什么必须拦截重复提交?

真实场景还原

某电商平台用户支付时因网络延迟连点3次“立即付款”,结果:

重复提交怎么拦截?

  • 系统创建了3笔相同订单
  • 库存扣减3次,导致超卖
  • 用户收到3条扣费短信,引发客诉

核心风险

  • 数据不一致(订单、库存、余额)
  • 系统资源浪费(重复调用外部接口)
  • 业务逻辑错乱(如优惠券被重复使用)

行业共识

重复提交是系统“慢性毒药”,99%的线上Bug源于未做拦截。


前端拦截方案(第一道防线)

1 按钮防抖(Debounce)

// Vue3 组合式API
const submit = debounce(async () => {
  await doSubmit()
}, 300) // 300ms内重复点击只执行最后一次

2 按钮置灰(最基础)

<button :disabled="isSubmitting" @click="handleSubmit">
  {{ isSubmitting ? '提交中...' : '提交' }}
</button>

⚠️ 前端方案的致命缺陷

  • 用户可绕过(F12控制台直接调用接口)
  • 断网重连后,请求被浏览器重放
  • 多窗口/多设备操作完全无效

前端拦截仅用于提升体验,不能取代后端防御


后端拦截方案(核心防线)

1 基于Token的防重复(最优解)

流程

  1. 服务端生成唯一token(UUID)
  2. 前端提交时携带token
  3. 后端用Redis检验:
    • 存在:删除token并执行业务
    • 不存在:返回“重复提交”

代码实现(SpringBoot):

@PostMapping("/pay")
public Result pay(@RequestParam String token) {
    Boolean success = redisTemplate.delete(token);
    if (!Boolean.TRUE.equals(success)) {
        return Result.error("订单正在处理,请勿重复提交");
    }
    // 执行支付逻辑
}

2 幂等表(适用于订单/转账)

CREATE TABLE idempotent (
    id BIGINT AUTO_INCREMENT,
    biz_key VARCHAR(128) UNIQUE, -- 业务唯一键(如订单号+操作)
    PRIMARY KEY (id)
);

原理:插入幂等表时利用唯一索引,重复插入报错。
适用场景:强一致性的金融、支付业务。

3 数据库乐观锁

UPDATE inventory 
SET stock = stock - 1 
WHERE id = #{id} AND stock > 0; -- 查是否实际修改行数
if (updateRows == 0) {
    throw new DuplicateSubmitException();
}

分布式场景下的终极方案(Redisson)

为什么需要分布式锁?

  • 单体Redis的 delete-then-execute 在并发下可能被绕过
  • 节点扩容后,锁必须全局生效

基于Redisson的防重复锁

RLock lock = redissonClient.getFairLock("order:lock:" + orderId);
try {
    if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
        // 执行核心业务
    } else {
        throw new DuplicateSubmitException("请求正在处理");
    }
} finally {
    lock.unlock();
}

Redisson优势

  • 自动续期(看门狗机制)
  • 公平锁防止饥饿
  • 支持哨兵/集群模式

高并发下的避坑指南

❌ 常见错误1:Redis锁过期

场景:业务执行超过锁的过期时间,锁自动释放导致重复提交。
方案:设置合适的过期时间 + Redisson自动续期。

❌ 常见错误2:ABA问题

场景:A请求删除token后,B请求恰好插入一个相同token。
方案:使用Redis的SET NX EX原子命令,key为业务唯一标识。

✅ 降级策略:融断 + 限流

# Sentinel规则
resources:
  submitOrder:
    degrade:
      count: 10
      timeWindow: 60 # 60秒内失败10次则融断

Q&A常见问题

Q1:用户反复刷新页面怎么办?

  • 前端:使用 history.replaceState() 禁止后退刷新
  • 后端:利用Token机制 + 前端请求携带上次的token

Q2:断网重连后请求被重放?

  • 方案:为每个请求生成 requestId(前端生成)
  • 后端校验:同一requestId只执行一次

Q3:接口被恶意刷单?

  • 出口:接入WAF(Web应用防火墙)
  • 入口:限制同一IP/设备调用的频率

实战代码一键复用

自定义注解 + AOP(推荐)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String key() default ""; // SpEL表达式(如 #order.orderId)
    long expire() default 10; // 锁过期时间
}

AOP切面

@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) {
    String uniqueKey = parseKey(idempotent, pjp.getArgs());
    RLock lock = redissonClient.getLock("idem:" + uniqueKey);
    boolean locked = lock.tryLock(0, idempotent.expire(), TimeUnit.SECONDS);
    if (!locked) {
        throw new DuplicateSubmitException();
    }
    return pjp.proceed();
}

使用示例

@Idempotent(key = "#order.id")
public String submitOrder(Order order) {
    // 业务逻辑
}

构建拦截重复提交的黄金法则

  1. 前端:按钮置灰 + 防抖(10%作用)
  2. 后端:Token机制 + 幂等表(80%作用)
  3. 分布式:Redisson分布式锁(99.9%作用)
  4. 监控:接入秒级告警(发现重复立即定位)

没有绝对的防重复,只有体系化的防御,建议组合使用2~3层方案,并做好日志记录,以便事后追溯。

最后一问:如果用户通过F12把前端disabled属性改掉怎么办?
答案:依赖后端防御!前端只是“提示器”,后端才是“守门员”。

抱歉,评论功能暂时关闭!