Java通過Caffeine和自定義注解實(shí)現(xiàn)本地防抖接口限流
一、背景與需求
在實(shí)際項(xiàng)目開發(fā)中,經(jīng)常遇到接口被前端高頻觸發(fā)、按鈕被多次點(diǎn)擊或者接口重復(fù)提交的問題,導(dǎo)致服務(wù)壓力變大、數(shù)據(jù)冗余、甚至引發(fā)冪等性/安全風(fēng)險(xiǎn)。
常規(guī)做法是前端節(jié)流/防抖、后端用Redis全局限流、或者API網(wǎng)關(guān)限流。但在很多場景下:
- 接口只要求單機(jī)(本地)防抖,不需要全局一致性;
- 只想讓同一個業(yè)務(wù)對象(同一手機(jī)號、同一業(yè)務(wù)ID、唯一標(biāo)識)在自定義設(shè)置秒內(nèi)只處理一次;
- 想要注解式配置,讓代碼更優(yōu)雅、好維護(hù)。
這個時(shí)候,Caffeine+自定義注解+AOP的本地限流(防抖)方案非常合適。
二、方案設(shè)計(jì)
1. Caffeine介紹
Caffeine 是目前Java領(lǐng)域最熱門、性能最高的本地內(nèi)存緩存庫,QPS可達(dá)百萬級,適用于低延遲、高并發(fā)、短TTL緩存場景。
在本地限流、防抖、接口去重等方面天然有優(yōu)勢。
2. 自定義注解+AOP
用自定義注解(如@DebounceLimit)標(biāo)記要防抖的接口,AOP切面攔截后判斷是否需要限流,核心思路是:
- 以唯一標(biāo)識作為key;
- 每次訪問接口,先查詢本地Caffeine緩存;
- 如果key在2秒內(nèi)已被處理過,則直接攔截;
- 否則執(zhí)行業(yè)務(wù)邏輯,并記錄處理時(shí)間。
這種方式無侵入、代碼簡潔、可擴(kuò)展性強(qiáng),適合絕大多數(shù)本地場景。
效果圖如下:
三、完整實(shí)現(xiàn)步驟
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表達(dá)式,如 #dto.id) */ String key(); /** * 防抖時(shí)間,單位秒 */ 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. 獲取方法、參數(shù) 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表達(dá)式得到唯一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); // 是否返回上次結(jié)果 if (debounceLimit.returnLastResult() && debounceResult.getResult() != null) { return debounceResult.getResult(); } // 統(tǒng)一失敗響應(yīng),可自定義異?;蚍祷亟Y(jié)構(gòu) 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. 控制器里直接用注解實(shí)現(xiàn)防抖
@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("在{}收到了請求,參數(shù)為:{}", DateUtil.now(), dto); return userService.getById(dto.getId()).getUsername(); } }
只要加了這個注解,同一個id的請求在自定義設(shè)置的秒內(nèi)只處理一次,其他直接被攔截并打印日志。
四、擴(kuò)展與注意事項(xiàng)
1.SpEL表達(dá)式靈活
可以用 #dto.id、#dto.mobile、#paramName等,非常適合多參數(shù)、復(fù)雜唯一性業(yè)務(wù)場景。
2.returnLastResult適合有“緩存返回結(jié)果”的場景
比如查詢接口、表單重復(fù)提交直接復(fù)用上次的返回值。
3.本地限流僅適用于單機(jī)環(huán)境
多節(jié)點(diǎn)部署建議用Redis分布式限流,原理一樣。
4.緩存key建議加上方法簽名
避免不同接口之間key沖突。
5.Caffeine最大緩存、過期時(shí)間應(yīng)根據(jù)業(yè)務(wù)并發(fā)和內(nèi)存合理設(shè)置
絕大多數(shù)接口幾千到幾萬key都沒壓力。
五、適用與不適用場景
適用:
- 單機(jī)接口防抖/限流
- 短時(shí)間重復(fù)提交防控
- 按業(yè)務(wù)唯一標(biāo)識維度防刷
- 秒殺、報(bào)名、投票等接口本地保護(hù)
不適用:
- 分布式場景(建議用Redis或API網(wǎng)關(guān)限流)
- 需要全局一致性的業(yè)務(wù)
- 內(nèi)存非常敏感/極端高并發(fā)下,需結(jié)合Redis做混合限流
六、總結(jié)
Caffeine + 注解 + AOP的本地限流防抖方案,實(shí)現(xiàn)簡單、代碼優(yōu)雅、性能極高、擴(kuò)展靈活
到此這篇關(guān)于Java通過Caffeine和自定義注解實(shí)現(xiàn)本地防抖接口限流的文章就介紹到這了,更多相關(guān)Java本地防抖接口限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java編程之多線程死鎖與線程間通信簡單實(shí)現(xiàn)代碼
這篇文章主要介紹了Java編程之多線程死鎖與線程間通信簡單實(shí)現(xiàn)代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10Java反射通過Getter方法獲取對象VO的屬性值過程解析
這篇文章主要介紹了Java反射通過Getter方法獲取對象VO的屬性值過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02mybaits-spring的實(shí)現(xiàn)方式
這篇文章主要介紹了mybaits-spring的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05IntelliJ IDEA 創(chuàng)建 Java 項(xiàng)目及創(chuàng)建 Java 文件并運(yùn)行的詳細(xì)步驟
這篇文章主要介紹了IntelliJ IDEA 創(chuàng)建 Java 項(xiàng)目及創(chuàng)建 Java 文件并運(yùn)行的詳細(xì)步驟,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11java實(shí)現(xiàn)微信App支付服務(wù)端
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)微信App支付服務(wù)端,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10Java中super與this關(guān)鍵字的用途及區(qū)別詳解
這篇文章主要介紹了Java中super與this關(guān)鍵字的用途及區(qū)別的相關(guān)資料,super和this是Java中用于引用父類和當(dāng)前對象的特殊關(guān)鍵字,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04Javaweb 500 服務(wù)器內(nèi)部錯誤的解決
這篇文章主要介紹了Javaweb 500 服務(wù)器內(nèi)部錯誤的解決方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09