別讓接口被瘋狂點(diǎn)擊!Spring Boot 防重實(shí)戰(zhàn):哈希 + 緩存雙保險(xiǎn)方案實(shí)測(cè)!
在高并發(fā)的業(yè)務(wù)場(chǎng)景中,接口被重復(fù)點(diǎn)擊或短時(shí)間內(nèi)多次提交請(qǐng)求,是一個(gè)常見(jiàn)但極具破壞性的隱患。 例如,電商系統(tǒng)中用戶點(diǎn)擊“提交訂單”按鈕多次,可能會(huì)生成重復(fù)訂單; 又如支付接口被多次觸發(fā),造成重復(fù)扣費(fèi); 或者表單接口因網(wǎng)絡(luò)抖動(dòng)被重新提交,產(chǎn)生臟數(shù)據(jù)。
這些問(wèn)題雖然看似小概率事件,但在真實(shí)生產(chǎn)環(huán)境中往往導(dǎo)致嚴(yán)重后果。 為了避免此類“重復(fù)提交”的混亂,我們需要在服務(wù)端層面構(gòu)建一個(gè)高可靠的防重機(jī)制。
本文將帶你實(shí)現(xiàn)一種“哈希 + 緩存”雙重保障的接口防重復(fù)提交方案, 無(wú)需前端配合,不依賴額外 Token,僅通過(guò)請(qǐng)求特征動(dòng)態(tài)生成哈希簽名,即可快速判斷重復(fù)請(qǐng)求。 我們將基于 Spring Boot + AOP + Redis/Caffeine 的架構(gòu)實(shí)現(xiàn)這一機(jī)制,輕量高效,實(shí)戰(zhàn)級(jí)可復(fù)用。
防重原理與方案選型
什么是防重復(fù)提交
防重復(fù)提交(Prevent Duplicate Request)指的是防止用戶在短時(shí)間內(nèi)對(duì)同一接口重復(fù)觸發(fā)操作,從而造成數(shù)據(jù)重復(fù)創(chuàng)建、狀態(tài)異常或邏輯錯(cuò)誤。
例如:
- 下單接口:防止同一個(gè)用戶同時(shí)創(chuàng)建兩筆相同訂單;
- 表單提交:防止頁(yè)面卡頓或多次點(diǎn)擊產(chǎn)生重復(fù)記錄;
- 支付操作:防止短時(shí)間內(nèi)重復(fù)支付。
常見(jiàn)實(shí)現(xiàn)方式
實(shí)現(xiàn)方式 | 原理說(shuō)明 | 優(yōu)缺點(diǎn) |
前端防重 | 按鈕加 loading,或禁用二次點(diǎn)擊 | 簡(jiǎn)單但不可靠,可被繞過(guò) |
Token 標(biāo)識(shí) | 每次請(qǐng)求生成唯一 Token,校驗(yàn)后銷毀 | 安全性高,但依賴前端 |
請(qǐng)求特征哈希(推薦) | 通過(guò)請(qǐng)求路徑、方法、參數(shù)生成唯一哈希值進(jìn)行校驗(yàn) | 無(wú)需前端依賴,后端即可防重 |
本文采用第三種方式,通過(guò) URL + 請(qǐng)求方法 + 請(qǐng)求參數(shù) 構(gòu)造一個(gè)全局唯一哈希值,并將其存儲(chǔ)在緩存中。 當(dāng)檢測(cè)到相同哈希在有效期內(nèi)再次出現(xiàn)時(shí),即判定為重復(fù)請(qǐng)求。
系統(tǒng)架構(gòu)與流程設(shè)計(jì)
目錄結(jié)構(gòu)如下
/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防重復(fù)機(jī)制核心流程如下:
- 請(qǐng)求進(jìn)入控制層;
- AOP 攔截目標(biāo)方法;
- 提取 URL、請(qǐng)求方法、參數(shù)信息;
- 計(jì)算 SHA-256 哈希值作為 Key;
- 查詢緩存(Redis/Caffeine)是否存在該 Key;
- 存在則拒絕請(qǐng)求,不存在則執(zhí)行方法并寫(xiě)入緩存。
核心實(shí)現(xiàn)代碼
自定義注解 @PreventDuplicate
package com.icoderoad.duplicate.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 防重復(fù)提交注解
* 可應(yīng)用在 Controller 層接口上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicate {
/** 防重復(fù)提交時(shí)間(單位:秒) */
int expire() default 3;
/** 時(shí)間單位,默認(rèn)秒 */
TimeUnit timeUnit() default TimeUnit.SECONDS;
/** 可選指定參與生成哈希的主要字段 */
String[] field() default {};
/** 提示信息 */
String message() default "請(qǐng)勿重復(fù)提交!";
}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("生成哈希耗時(shí):" + (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 "防重復(fù)測(cè)試:" + name + " " + age + " " + address;
}
@PostMapping("/saveUserInfo")
@PreventDuplicate(expire = 5)
public String saveUserInfo(@RequestBody UserInfo userInfo) {
System.out.println(userInfo);
return "請(qǐng)求時(shí)間:" + DateTime.now() + " 保存成功";
}
@PostMapping("/saveContent")
@PreventDuplicate(expire = 10)
public String saveContent(@RequestBody ArticleDTO articleDTO) {
System.out.println(articleDTO);
return "請(qǐng)求時(shí)間:" + DateTime.now() + " 內(nèi)容保存成功";
}
}測(cè)試結(jié)果: 當(dāng)短時(shí)間內(nèi)重復(fù)發(fā)送相同參數(shù)請(qǐng)求時(shí),系統(tǒng)將直接返回
"請(qǐng)勿重復(fù)提交!"異常提示。
性能驗(yàn)證
為了驗(yàn)證哈希計(jì)算的性能,我們生成了一篇 3 萬(wàn)字文章內(nèi)容并進(jìn)行請(qǐng)求測(cè)試。 結(jié)果顯示:
- 首次生成哈希值耗時(shí)約 9ms(JVM 預(yù)熱階段);
- 多次請(qǐng)求后平均耗時(shí)降至 0ms;
- 即使請(qǐng)求參數(shù)極大,對(duì)性能幾乎無(wú)影響。
結(jié)論: SHA-256 哈希算法在防重場(chǎng)景中既具唯一性又具高性能,完全可滿足高并發(fā)接口防重復(fù)的需求。
總結(jié)與實(shí)踐建議
通過(guò)本方案,我們實(shí)現(xiàn)了一個(gè)無(wú)侵入、通用性強(qiáng)、性能優(yōu)異的防重復(fù)提交機(jī)制。 核心要點(diǎn)包括:
- 使用 AOP 切面攔截 請(qǐng)求,避免侵入業(yè)務(wù)邏輯;
- 基于 請(qǐng)求路徑 + 方法 + 參數(shù)哈希 生成唯一標(biāo)識(shí);
- 通過(guò) Redis / Caffeine 緩存 實(shí)現(xiàn)分布式與本地防重雙模式;
- 支持靈活配置提交間隔與關(guān)鍵字段粒度。
該方案不僅可用于表單、下單、支付等關(guān)鍵接口,也可擴(kuò)展至異步任務(wù)提交、API 冪等控制等更廣泛場(chǎng)景。
未來(lái)還可以進(jìn)一步優(yōu)化:
- 加入 異步清理機(jī)制;
- 對(duì) Key 結(jié)構(gòu)添加命名空間前綴;
- 結(jié)合 分布式鎖 提升在集群環(huán)境下的安全性。
一句話總結(jié):
防重不是“錦上添花”的優(yōu)化,而是“防止災(zāi)難”的必要保護(hù)。 用哈希 + 緩存雙保險(xiǎn),為你的接口上好“安全帶”!

































