沉默是金,總會發光

大家好,我是沉默


不知道你有沒有遇到過這種線上事故:

  • 一個用户下了 兩筆一模一樣的訂單
  • 支付接口被點了 三次,錢扣了三次
  • 抽獎活動一上線,獎品 10 秒被薅光


最後排查半天,發現原因只有一個
用户點快了,接口沒兜住。


在真實的線上系統裏,
重複提交不是“偶發問題”,而是“必然事件”

  • 網絡抖一下,用户以為沒點上
  • 頁面沒反饋,用户瘋狂點
  • 接口慢 2 秒,用户直接刷新重試

 

如果系統沒有防護,就等於在賭用户的手速和耐心。


所以今天這篇文章,我們不講花活,
系統性梳理「前後端防重複提交的主流方案」
從“能用”到“線上可扛”,一次講透。



-01-

為什麼防重複提交前後端都要做? 

一句話(面試可背)

前端能減少誤操作,後端是系統的最終防線。

如果你什麼都不做,重複提交會直接導致:

  •  訂單重複生成
  •  支付重複扣款
  •  抽獎次數被多次消耗
  •  數據庫出現髒數據、對賬困難

本質問題只有一個:接口不是冪等的。

面試官:説説你們是怎麼做防重複提交的?_ios



-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 機制(經典永不過時)

  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("操作過於頻繁");
}

 分佈式友好
 無侵入
 統一治理

方案三:攔截器統一兜底

適合 全局統一防護策略,不想每個接口都加註解。


面試官:説説你們是怎麼做防重複提交的?_重複提交_02


-03-

高併發場景

Redis + Lua 保證原子性

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


面試官:説説你們是怎麼做防重複提交的?_重複提交_03



-04-

總結

方案對比總表(收藏版)

方案

是否推薦

適用場景

優點

缺點

前端禁用按鈕

輔助

所有表單

體驗好

可繞過

Session Token

推薦

單機 / Session 共享

安全經典

分佈式複雜

AOP + Redis

強烈推薦

微服務 / 集羣

無侵入、穩定

依賴 Redis

攔截器 + Redis

推薦

統一治理

集中管理

靈活性稍低

數據庫唯一索引

輔助

強一致約束

簡單

請求已進系統

最佳實踐(架構師建議)

  • 前端 + 後端一定要配合
  • 表單類操作:Token 優先
  • 分佈式系統:AOP + Redis 是首選
  • 鎖時間:5~10 秒足夠
  • 提示信息要友好,別甩 500
  • 防重 ≠ 冪等,複雜業務要單獨設計

防重複提交,本質不是“多寫幾行代碼”,
而是 系統穩定性與數據一致性的基本功

不要把安全寄託在用户不手滑上。
真正成熟的系統,永遠是:

用户可以亂點,系統不能亂。


如果你覺得這篇文章有用,
歡迎點個 👍 / 轉發 / 評論區一起補充實戰坑。


面試官:説説你們是怎麼做防重複提交的?_ios_04





-05-

粉絲福利




點點關注,送你互聯網大廠面試題庫,如果你正在找工作,又或者剛準備換工作。可以仔細閲讀一下,或許對你有所幫助!


面試官:説説你們是怎麼做防重複提交的?_ios_05

面試官:説説你們是怎麼做防重複提交的?_Redis_06

面試官:説説你們是怎麼做防重複提交的?_Redis_07