沉默是金,總會發光
大家好,我是沉默
不知道你有沒有遇到過這種線上事故:
- 一個用户下了 兩筆一模一樣的訂單
- 支付接口被點了 三次,錢扣了三次
- 抽獎活動一上線,獎品 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-
粉絲福利
點點關注,送你互聯網大廠面試題庫,如果你正在找工作,又或者剛準備換工作。可以仔細閲讀一下,或許對你有所幫助!