別讓接口被瘋狂點擊!Spring Boot 防重實戰:哈希 + 緩存雙保險方案實測!
在高并發的業務場景中,接口被重復點擊或短時間內多次提交請求,是一個常見但極具破壞性的隱患。 例如,電商系統中用戶點擊“提交訂單”按鈕多次,可能會生成重復訂單; 又如支付接口被多次觸發,造成重復扣費; 或者表單接口因網絡抖動被重新提交,產生臟數據。
這些問題雖然看似小概率事件,但在真實生產環境中往往導致嚴重后果。 為了避免此類“重復提交”的混亂,我們需要在服務端層面構建一個高可靠的防重機制。
本文將帶你實現一種“哈希 + 緩存”雙重保障的接口防重復提交方案, 無需前端配合,不依賴額外 Token,僅通過請求特征動態生成哈希簽名,即可快速判斷重復請求。 我們將基于 Spring Boot + AOP + Redis/Caffeine 的架構實現這一機制,輕量高效,實戰級可復用。
防重原理與方案選型
什么是防重復提交
防重復提交(Prevent Duplicate Request)指的是防止用戶在短時間內對同一接口重復觸發操作,從而造成數據重復創建、狀態異常或邏輯錯誤。
例如:
- 下單接口:防止同一個用戶同時創建兩筆相同訂單;
- 表單提交:防止頁面卡頓或多次點擊產生重復記錄;
- 支付操作:防止短時間內重復支付。
常見實現方式
實現方式 | 原理說明 | 優缺點 |
前端防重 | 按鈕加 loading,或禁用二次點擊 | 簡單但不可靠,可被繞過 |
Token 標識 | 每次請求生成唯一 Token,校驗后銷毀 | 安全性高,但依賴前端 |
請求特征哈希(推薦) | 通過請求路徑、方法、參數生成唯一哈希值進行校驗 | 無需前端依賴,后端即可防重 |
本文采用第三種方式,通過 URL + 請求方法 + 請求參數 構造一個全局唯一哈希值,并將其存儲在緩存中。 當檢測到相同哈希在有效期內再次出現時,即判定為重復請求。
系統架構與流程設計
目錄結構如下
/src
└── /main
├── /java/com/icoderoad/duplicate
│ ├── annotation/PreventDuplicate.java
│ ├── aspect/PreventDuplicateAspect.java
│ ├── storage/DuplicateStorage.java
│ ├── storage/impl/RedisStorage.java
│ ├── storage/impl/CaffeineStorage.java
│ └── util/RequestParameterUtils.java
└── /resources
└── application.yml防重復機制核心流程如下:
- 請求進入控制層;
- AOP 攔截目標方法;
- 提取 URL、請求方法、參數信息;
- 計算 SHA-256 哈希值作為 Key;
- 查詢緩存(Redis/Caffeine)是否存在該 Key;
- 存在則拒絕請求,不存在則執行方法并寫入緩存。
核心實現代碼
自定義注解 @PreventDuplicate
package com.icoderoad.duplicate.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 防重復提交注解
* 可應用在 Controller 層接口上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicate {
/** 防重復提交時間(單位:秒) */
int expire() default 3;
/** 時間單位,默認秒 */
TimeUnit timeUnit() default TimeUnit.SECONDS;
/** 可選指定參與生成哈希的主要字段 */
String[] field() default {};
/** 提示信息 */
String message() default "請勿重復提交!";
}AOP 攔截器 PreventDuplicateAspect
package com.icoderoad.duplicate.aspect;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.core.date.DateTime;
import com.icoderoad.duplicate.annotation.PreventDuplicate;
import com.icoderoad.duplicate.storage.DuplicateStorage;
import com.icoderoad.duplicate.storage.DuplicateStorageFactory;
import com.icoderoad.duplicate.util.RequestParameterUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
@RequiredArgsConstructor
public class PreventDuplicateAspect {
private final HttpServletRequest request;
private final DuplicateStorageFactory storageFactory;
@Around("@annotation(preventDuplicate)")
public Object handle(ProceedingJoinPoint joinPoint, PreventDuplicate preventDuplicate) throws Throwable {
String method = request.getMethod();
String uri = request.getRequestURI();
String params = RequestParameterUtils.getAllParamsAsString(joinPoint, preventDuplicate.field());
// 拼接唯一簽名源
String signSource = method + ":" + uri + ":" + params;
long start = System.currentTimeMillis();
String key = DigestUtil.sha256Hex(signSource);
long end = System.currentTimeMillis();
System.out.println("生成哈希耗時:" + (end - start) + "ms");
DuplicateStorage storage = storageFactory.getStorage();
if (storage.exists(key)) {
throw new RuntimeException(preventDuplicate.message());
}
storage.put(key, preventDuplicate.expire(), preventDuplicate.timeUnit());
return joinPoint.proceed();
}
}控制層示例
package com.icoderoad.duplicate.controller;
import cn.hutool.core.date.DateTime;
import com.icoderoad.duplicate.annotation.PreventDuplicate;
import com.icoderoad.duplicate.model.ArticleDTO;
import com.icoderoad.duplicate.model.UserInfo;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
@PreventDuplicate
public String hello(String name, String age, String address) {
return "防重復測試:" + name + " " + age + " " + address;
}
@PostMapping("/saveUserInfo")
@PreventDuplicate(expire = 5)
public String saveUserInfo(@RequestBody UserInfo userInfo) {
System.out.println(userInfo);
return "請求時間:" + DateTime.now() + " 保存成功";
}
@PostMapping("/saveContent")
@PreventDuplicate(expire = 10)
public String saveContent(@RequestBody ArticleDTO articleDTO) {
System.out.println(articleDTO);
return "請求時間:" + DateTime.now() + " 內容保存成功";
}
}測試結果: 當短時間內重復發送相同參數請求時,系統將直接返回
"請勿重復提交!"異常提示。
性能驗證
為了驗證哈希計算的性能,我們生成了一篇 3 萬字文章內容并進行請求測試。 結果顯示:
- 首次生成哈希值耗時約 9ms(JVM 預熱階段);
- 多次請求后平均耗時降至 0ms;
- 即使請求參數極大,對性能幾乎無影響。
結論: SHA-256 哈希算法在防重場景中既具唯一性又具高性能,完全可滿足高并發接口防重復的需求。
總結與實踐建議
通過本方案,我們實現了一個無侵入、通用性強、性能優異的防重復提交機制。 核心要點包括:
- 使用 AOP 切面攔截 請求,避免侵入業務邏輯;
- 基于 請求路徑 + 方法 + 參數哈希 生成唯一標識;
- 通過 Redis / Caffeine 緩存 實現分布式與本地防重雙模式;
- 支持靈活配置提交間隔與關鍵字段粒度。
該方案不僅可用于表單、下單、支付等關鍵接口,也可擴展至異步任務提交、API 冪等控制等更廣泛場景。
未來還可以進一步優化:
- 加入 異步清理機制;
- 對 Key 結構添加命名空間前綴;
- 結合 分布式鎖 提升在集群環境下的安全性。
一句話總結:
防重不是“錦上添花”的優化,而是“防止災難”的必要保護。 用哈希 + 緩存雙保險,為你的接口上好“安全帶”!
































