沉默是金,总会发光
大家好,我是沉默
不知道你有没有遇到过这种线上事故:
-
一个用户下了 两笔一模一样的订单
-
支付接口被点了 三次,钱扣了三次
-
抽奖活动一上线,奖品 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(this, arguments);
}, 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 机制(经典永不过时)
- 服务端生成 Token
- 前端提交时携带
- 校验通过后 立即删除
核心代码
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-
粉丝福利
点点关注,送你互联网大厂面试题库,如果你正在找工作,又或者刚准备换工作。可以仔细阅读一下,或许对你有所帮助!