Java通過Caffeine和自定義注解實現本地防抖接口限流
一、背景與需求
在實際項目開發(fā)中,經常遇到接口被前端高頻觸發(fā)、按鈕被多次點擊或者接口重復提交的問題,導致服務壓力變大、數據冗余、甚至引發(fā)冪等性/安全風險。
常規(guī)做法是前端節(jié)流/防抖、后端用Redis全局限流、或者API網關限流。但在很多場景下:
- 接口只要求單機(本地)防抖,不需要全局一致性;
- 只想讓同一個業(yè)務對象(同一手機號、同一業(yè)務ID、唯一標識)在自定義設置秒內只處理一次;
- 想要注解式配置,讓代碼更優(yōu)雅、好維護。
這個時候,Caffeine+自定義注解+AOP的本地限流(防抖)方案非常合適。
二、方案設計
1. Caffeine介紹
Caffeine 是目前Java領域最熱門、性能最高的本地內存緩存庫,QPS可達百萬級,適用于低延遲、高并發(fā)、短TTL緩存場景。
在本地限流、防抖、接口去重等方面天然有優(yōu)勢。
2. 自定義注解+AOP
用自定義注解(如@DebounceLimit)標記要防抖的接口,AOP切面攔截后判斷是否需要限流,核心思路是:
- 以唯一標識作為key;
- 每次訪問接口,先查詢本地Caffeine緩存;
- 如果key在2秒內已被處理過,則直接攔截;
- 否則執(zhí)行業(yè)務邏輯,并記錄處理時間。
這種方式無侵入、代碼簡潔、可擴展性強,適合絕大多數本地場景。
效果圖如下:

三、完整實現步驟
1.Pom依賴如下
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>2. 定義自定義注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DebounceLimit {
/**
* 唯一key(支持SpEL表達式,如 #dto.id)
*/
String key();
/**
* 防抖時間,單位秒
*/
int ttl() default 2;
/**
* 是否返回上次緩存的返回值
*/
boolean returnLastResult() default true;
}3. 配置Caffeine緩存Bean
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class DebounceCacheConfig {
@Bean
public Cache<String, Object> debounceCache() {
return Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100_000)
.build();
}
}4. 編寫AOP切面
import com.github.benmanes.caffeine.cache.Cache;
import com.lps.anno.DebounceLimit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class DebounceLimitAspect {
@Autowired
private Cache<String, Object> debounceCache;
private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(debounceLimit)")
public Object around(ProceedingJoinPoint pjp, DebounceLimit debounceLimit) throws Throwable {
// 1. 獲取方法、參數
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
Object[] args = pjp.getArgs();
String[] paramNames = methodSignature.getParameterNames();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 2. 解析SpEL表達式得到唯一key
String key = parser.parseExpression(debounceLimit.key()).getValue(context, String.class);
String cacheKey = method.getDeclaringClass().getName() + "." + method.getName() + ":" + key;
long now = System.currentTimeMillis();
DebounceResult<Object> debounceResult = (DebounceResult<Object>) debounceCache.getIfPresent(cacheKey);
if (debounceResult != null && (now - debounceResult.getTimestamp() < debounceLimit.ttl() * 1000L)) {
String methodName = pjp.getSignature().toShortString();
log.error("接口[{}]被限流, key={}", methodName, cacheKey);
// 是否返回上次結果
if (debounceLimit.returnLastResult() && debounceResult.getResult() != null) {
return debounceResult.getResult();
}
// 統(tǒng)一失敗響應,可自定義異?;蚍祷亟Y構
return new RuntimeException("操作過于頻繁,請稍后再試!");
}
Object result = pjp.proceed();
debounceCache.put(cacheKey, new DebounceResult<>(result, now));
return result;
}
@Getter
static class DebounceResult<T> {
private final T result;
private final long timestamp;
public DebounceResult(T result, long timestamp) {
this.result = result;
this.timestamp = timestamp;
}
}
}5. 控制器里直接用注解實現防抖
@RestController
@RequiredArgsConstructor
@Slf4j
public class DebounceControl {
private final UserService userService;
@PostMapping("/getUsernameById")
@DebounceLimit(key = "#dto.id", ttl = 10)
public String test(@RequestBody User dto) {
log.info("在{}收到了請求,參數為:{}", DateUtil.now(), dto);
return userService.getById(dto.getId()).getUsername();
}
}只要加了這個注解,同一個id的請求在自定義設置的秒內只處理一次,其他直接被攔截并打印日志。
四、擴展與注意事項
1.SpEL表達式靈活
可以用 #dto.id、#dto.mobile、#paramName等,非常適合多參數、復雜唯一性業(yè)務場景。
2.returnLastResult適合有“緩存返回結果”的場景
比如查詢接口、表單重復提交直接復用上次的返回值。
3.本地限流僅適用于單機環(huán)境
多節(jié)點部署建議用Redis分布式限流,原理一樣。
4.緩存key建議加上方法簽名
避免不同接口之間key沖突。
5.Caffeine最大緩存、過期時間應根據業(yè)務并發(fā)和內存合理設置
絕大多數接口幾千到幾萬key都沒壓力。
五、適用與不適用場景
適用:
- 單機接口防抖/限流
- 短時間重復提交防控
- 按業(yè)務唯一標識維度防刷
- 秒殺、報名、投票等接口本地保護
不適用:
- 分布式場景(建議用Redis或API網關限流)
- 需要全局一致性的業(yè)務
- 內存非常敏感/極端高并發(fā)下,需結合Redis做混合限流
六、總結
Caffeine + 注解 + AOP的本地限流防抖方案,實現簡單、代碼優(yōu)雅、性能極高、擴展靈活
到此這篇關于Java通過Caffeine和自定義注解實現本地防抖接口限流的文章就介紹到這了,更多相關Java本地防抖接口限流內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
關于SpringBoot中Ajax跨域以及Cookie無法獲取丟失問題
這篇文章主要介紹了關于SpringBoot中Ajax跨域以及Cookie無法獲取丟失問題,本文具有參考意義,遇到相同或者類似問題的小伙伴希望可以從中找到靈感2023-03-03
Spring?Security?OAuth?Client配置加載源碼解析
這篇文章主要為大家介紹了Spring?Security?OAuth?Client配置加載源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07

