最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java通過Caffeine和自定義注解實(shí)現(xiàn)本地防抖接口限流

 更新時(shí)間:2025年05月20日 08:32:33   作者:不掉頭發(fā)的阿水  
Caffeine?是目前Java領(lǐng)域最熱門,性能最高的本地內(nèi)存緩存庫,本文為大家詳細(xì)介紹了如何利用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ī)(本地)防抖,不需要全局一致性;
  • 只想讓同一個(gè)業(yè)務(wù)對象(同一手機(jī)號、同一業(yè)務(wù)ID、唯一標(biāo)識)在自定義設(shè)置秒內(nèi)只處理一次;
  • 想要注解式配置,讓代碼更優(yōu)雅、好維護(hù)。

這個(gè)時(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();
    }
}

只要加了這個(gè)注解,同一個(gè)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)文章

  • Spring中的@Transactional事務(wù)失效場景解讀

    Spring中的@Transactional事務(wù)失效場景解讀

    這篇文章主要介紹了Spring中的@Transactional事務(wù)失效場景解讀,如果Transactional注解應(yīng)用在非public 修飾的方法上,Transactional將會(huì)失效此方法會(huì)檢查目標(biāo)方法的修飾符是否為 public,不是 public則不會(huì)獲取@Transactional 的屬性配置信息,需要的朋友可以參考下
    2023-12-12
  • 關(guān)于Spring @Bean 相同加載順序不同結(jié)果不同的問題記錄

    關(guān)于Spring @Bean 相同加載順序不同結(jié)果不同的問題記錄

    本文主要探討了在Spring 5.1.3.RELEASE版本下,當(dāng)有兩個(gè)全注解類定義相同類型的Bean時(shí),由于加載順序不同,最終生成的Bean實(shí)例也會(huì)不同,文章通過分析ConfigurationClassPostProcessor的執(zhí)行過程,解釋了BeanDefinition的加載和覆蓋機(jī)制,感興趣的朋友一起看看吧
    2025-02-02
  • Springboot詳解RocketMQ實(shí)現(xiàn)廣播消息流程

    Springboot詳解RocketMQ實(shí)現(xiàn)廣播消息流程

    RocketMQ作為一款純java、分布式、隊(duì)列模型的開源消息中間件,支持事務(wù)消息、順序消息、批量消息、定時(shí)消息、消息回溯等,本篇我們了解如何實(shí)現(xiàn)廣播消息
    2022-06-06
  • 最好的Java 反編譯工具的使用對比分析

    最好的Java 反編譯工具的使用對比分析

    恰好最近工作中也需要用到 Java 反編譯,所以這篇文章介紹目前常見的的幾種 Java 反編譯工具的使用,在文章的最后也會(huì)通過編譯速度、語法支持以及代碼可讀性三個(gè)維度,對它們進(jìn)行測試,分析幾款工具的優(yōu)缺點(diǎn),感興趣的朋友一起看看吧
    2021-05-05
  • Spring中的父子容器原理解析

    Spring中的父子容器原理解析

    這篇文章主要為大家介紹了Spring中的父子容器原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • java利用注解實(shí)現(xiàn)簡單的excel數(shù)據(jù)讀取

    java利用注解實(shí)現(xiàn)簡單的excel數(shù)據(jù)讀取

    這篇文章主要為大家詳細(xì)介紹了java利用注解實(shí)現(xiàn)簡單的excel數(shù)據(jù)讀取,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • MyBatis的動(dòng)態(tài)SQL語句實(shí)現(xiàn)

    MyBatis的動(dòng)態(tài)SQL語句實(shí)現(xiàn)

    這篇文章主要介紹了MyBatis的動(dòng)態(tài)SQL語句實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-01-01
  • SpringBoot操作MaxComputer方式(保姆級教程)

    SpringBoot操作MaxComputer方式(保姆級教程)

    這篇文章主要介紹了SpringBoot操作MaxComputer方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-03-03
  • zookeeper概述圖文詳解

    zookeeper概述圖文詳解

    今天小編就為大家分享一篇關(guān)于Zookeeper概述圖文詳解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • SpringBoot如何使用@Value取配置文件中的map配置

    SpringBoot如何使用@Value取配置文件中的map配置

    這篇文章主要介紹了SpringBoot如何使用@Value取配置文件中的map配置方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-05-05

最新評論

监利县| 达日县| 沈阳市| 邻水| 宁河县| 邵阳市| 临沭县| 漾濞| 高青县| 宜昌市| 隆回县| 郎溪县| 宁南县| 惠水县| 和政县| 凤阳县| 霍山县| 开平市| 兴山县| 伊春市| 广平县| 凤台县| 永兴县| 德保县| 开阳县| 田阳县| 临安市| 赞皇县| 西畴县| 额敏县| 东乡族自治县| 石屏县| 南澳县| 太和县| 安顺市| 旌德县| 南靖县| 年辖:市辖区| 嵩明县| 图木舒克市| 安徽省|