面试官:说说你们是怎么做防重复提交的?

24 阅读4分钟

沉默是金,总会发光

大家好,我是沉默

不知道你有没有遇到过这种线上事故:

  • 一个用户下了 两笔一模一样的订单

  • 支付接口被点了 三次,钱扣了三次

  • 抽奖活动一上线,奖品 10 秒被薅光

最后排查半天,发现原因只有一个
用户点快了,接口没兜住。

在真实的线上系统里,
重复提交不是“偶发问题”,而是“必然事件”

  • 网络抖一下,用户以为没点上
  • 页面没反馈,用户疯狂点
  • 接口慢 2 秒,用户直接刷新重试

 **
**

如果系统没有防护,就等于在赌用户的手速和耐心。

所以今天这篇文章,我们不讲花活,
系统性梳理「前后端防重复提交的主流方案」
从“能用”到“线上可扛”,一次讲透。

**-**01-

为什么防重复提交前后端都要做?

一句话(面试可背)

前端能减少误操作,后端是系统的最终防线。

如果你什么都不做,重复提交会直接导致:

  •  订单重复生成
  •  支付重复扣款
  •  抽奖次数被多次消耗
  •  数据库出现脏数据、对账困难

本质问题只有一个:接口不是幂等的。

图片

- 02-

前后端防重复提交

前端防重复提交(第一道防线)

目标只有一个:别让用户手滑那么容易成功

1. 提交后立即禁用按钮(最常见)

<buttonid="submitBtn"onclick="submitForm()">提交</button>
functionsubmitForm() {
const btn = document.getElementById("submitBtn");
if (btn.disabled) return;

    btn.disabled = true;
    btn.innerText = "提交中...";

fetch("/order/submit", {
method: "POST",
body: newFormData(document.getElementById("orderForm"))
    }).finally(() => {
        btn.disabled = false;
        btn.innerText = "提交";
    });
}

优点

  • 成本低
  • 用户体验好

缺点(致命)

  • F12 一开,JS 一改,直接绕过

只能算“礼貌性防御”

2. 按钮防抖(Debounce)

functiondebounce(func, wait) {
let timeout;
returnfunction () {
clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(thisarguments);
    }, wait);
  };
}

const submitForm = debounce(() => {
// 提交逻辑
}, 1000);

 本质是:
1 秒内,你点多少次,我都只认一次

3. 请求拦截(Axios 层防重)

const pendingRequests = newMap();

axios.interceptors.request.use(config => {
const key = config.url + JSON.stringify(config.data);
if (pendingRequests.has(key)) {
returnPromise.reject("请勿重复提交");
  }
  pendingRequests.set(key, true);
return config;
});

axios.interceptors.response.use(res => {
const key = res.config.url + JSON.stringify(res.config.data);
  pendingRequests.delete(key);
return res;
});

 前端方案总结一句话

提升体验可以,别指望它兜底安全。

后端防重复提交

方案一:Token 机制(经典永不过时)

  1. 服务端生成 Token
  2. 前端提交时携带
  3. 校验通过后 立即删除

核心代码

public String generateToken(HttpServletRequest request) {
Stringtoken= UUID.randomUUID().toString();
    request.getSession().setAttribute("FORM_TOKEN", token);
return token;
}

publicbooleanvalidateToken(HttpServletRequest request) {
StringclientToken= request.getParameter("token");
StringserverToken= (String) request.getSession().getAttribute("FORM_TOKEN");

if (!Objects.equals(clientToken, serverToken)) {
returnfalse;
    }
    request.getSession().removeAttribute("FORM_TOKEN");
returntrue;
}

 安全
简单
分布式需 Session 共享 / Redis

方案二:AOP + Redis(强烈推荐

现在 90% 的生产系统,都该用这个

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface NoRepeatSubmit {
intlockTime()default5;
}

核心思想

  • 用户 + 接口 + 参数 → 唯一 Key
  • Redis SETNX + EXPIRE
  • 拦截重复请求
Booleanlocked= redisTemplate.opsForValue()
    .setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);

if (!locked) {
thrownewRuntimeException("操作过于频繁");
}

 分布式友好
无侵入
统一治理

方案三:拦截器统一兜底

适合 全局统一防护策略,不想每个接口都加注解。

图片

- 03-

高并发场景

Redis + Lua 保证原子性

if redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') then
return1
else
return0
end



防止「锁刚加上,服务挂了」这种线上事故。

图片

**-**04-

总结

方案对比总表(收藏版)

方案是否推荐适用场景优点缺点
前端禁用按钮辅助所有表单体验好可绕过
Session Token推荐单机 / Session 共享安全经典分布式复杂
AOP + Redis强烈推荐微服务 / 集群无侵入、稳定依赖 Redis
拦截器 + Redis推荐统一治理集中管理灵活性稍低
数据库唯一索引辅助强一致约束简单请求已进系统

最佳实践(架构师建议)

  • 前端 + 后端一定要配合
  • 表单类操作:Token 优先
  • 分布式系统:AOP + Redis 是首选
  • 锁时间:5~10 秒足够
  • 提示信息要友好,别甩 500
  • 防重 ≠ 幂等,复杂业务要单独设计

防重复提交,本质不是“多写几行代码”,
而是 系统稳定性与数据一致性的基本功

不要把安全寄托在用户不手滑上。
真正成熟的系统,永远是:

用户可以乱点,系统不能乱。

如果你觉得这篇文章有用,
欢迎点个 👍 / 转发 / 评论区一起补充实战坑。

图片

**-**05-

粉丝福利

点点关注,送你互联网大厂面试题库,如果你正在找工作,又或者刚准备换工作。可以仔细阅读一下,或许对你有所帮助!

image.png

image.pngimage.png