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

Spring AOP + Guava RateLimiter實現(xiàn)優(yōu)雅限流

 更新時間:2026年04月02日 08:15:55   作者:一旅人  
本文主要介紹了Spring AOP + Guava RateLimiter實現(xiàn)優(yōu)雅限流,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

寫在前面

提起 AOP(面向切面編程),大家的第一反應(yīng)往往是:“哦,那個用來打印日志、管理事務(wù)、或者做權(quán)限校驗的。”

其實,AOP 的能力遠不止于此。在面對高并發(fā)場景下的接口自我保護時,它同樣能發(fā)揮奇效。

最近在項目中遇到了一個真實場景:這是一個基于 MQ 觸發(fā)的定時跑批任務(wù)。平日里風平浪靜,可是一旦大促或者數(shù)據(jù)量激增,MQ 里的積壓消息就會瞬間推送給消費者。

雖然消費者服務(wù)雖然處理得過來,但底層的核心業(yè)務(wù)數(shù)據(jù)庫卻扛不住了——大量并發(fā)查詢瞬間打滿 CPU,CPU 使用率飆升至 100%,直接影響了線上實時業(yè)務(wù)的穩(wěn)定性。

考慮到該服務(wù)是單節(jié)點部署,引入 Redis 做分布式限流顯得“殺雞用牛刀”,也增加了額外的運維成本。最終,我決定使用 Spring AOP + Guava RateLimiter + 自定義注解,實現(xiàn)一個 無侵入、可配置、輕量級單機限流組件。

一、 為什么選擇 AOP + 注解?

在介紹代碼之前,先明確設(shè)計初衷。

以前我剛接觸開發(fā)時,也喜歡在 Service 或 Controller 層直接硬編碼限流邏輯,例如:

// ? 反例:硬編碼,邏輯混雜且難以復用
if (!rateLimiter.tryAcquire()) {
    throw new RuntimeException("系統(tǒng)繁忙");
}
doBusiness();

這種寫法的弊端很明顯:

  1. 邏輯混雜:清晰的業(yè)務(wù)代碼中夾雜著非業(yè)務(wù)的限流判斷。
  2. 復用性差:如果有十個接口需要限流,就需要重復編寫十次。
  3. 維護困難:一旦需要調(diào)整限流策略(例如升級為分布式限流),涉及的修改點將非常多。

AOP(面向切面編程) 的核心就是 “解耦”“復用”。

我將限流邏輯封裝為一個獨立的“切面”,配合自定義注解作為“開關(guān)”。只需在目標方法上添加一個注解,限流策略隨即生效。后續(xù)的維護與升級,也僅需聚焦于切面邏輯本身,無需觸碰任何業(yè)務(wù)代碼。

二、 Guava RateLimiter 核心原理

我這次選用的核心庫是 Google Guava 的 RateLimiter。它是基于 令牌桶算法(Token Bucket) 實現(xiàn)的。

1. 簡單回顧令牌桶

它的機制不像“漏桶”那樣死板(恒定速率流出),而是更加人性化:

  • 生產(chǎn)令牌:系統(tǒng)以固定速率向桶中放入令牌。
  • 消費令牌:請求過來時,必須先拿到令牌才能執(zhí)行。
  • 關(guān)鍵特性支持突發(fā)流量。如果一段時間沒有請求,桶里的令牌會積攢起來(直到達到桶上限)。當一波突發(fā)流量到來時,可以直接消耗積攢的令牌立刻執(zhí)行,而不需要排隊等待。

2. 兩種核心模式

Guava 貼心地提供了兩種實現(xiàn):

  1. SmoothBursty(平滑突發(fā))默認模式。適合大多數(shù)場景,允許短時間的流量突發(fā)。
  2. SmoothWarmingUp(平滑預熱)預熱模式。啟動初期令牌發(fā)放速率較慢,隨著時間推移逐步提升到目標 QPS。這對于需要“熱身”的資源(如數(shù)據(jù)庫連接池、緩存填充)非常友好,防止冷啟動時瞬間被打掛。

3. 單機版警告 ??

注意Guava RateLimiter單機限流 工具!令牌是存在當前 JVM 內(nèi)存里的。

  • 如果你的服務(wù)只部署一臺機器,它完美勝任。
  • 如果你部署了 10 臺機器,每臺設(shè)置 QPS=5,那么整個集群的總 QPS 上限是 50。

4. 常用 API 詳解

熟練掌握 API 是實戰(zhàn)的基礎(chǔ),以下是 RateLimiter 的核心方法:

核心創(chuàng)建方法

方法簽名說明
create(double permitsPerSecond)創(chuàng)建 SmoothBursty 限流器,指定每秒生成的令牌數(shù)(默認:permitsPerSecond = QPS = 桶容量)。
create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)創(chuàng)建 SmoothWarmingUp 限流器,指定 QPS + 預熱時間。

核心獲取方法

方法簽名說明
double acquire()阻塞式獲取 1 個令牌。若無令牌,線程會一直等待,直到獲取成功。
double acquire(int permits)阻塞式獲取指定數(shù)量的令牌(可一次獲取多個)。
boolean tryAcquire()非阻塞式獲取 1 個令牌。立即返回:成功 true,失敗 false(不等待)。
boolean tryAcquire(long timeout, TimeUnit unit)限時等待獲取 1 個令牌。在超時時間內(nèi)拿到返回 true,否則返回 false。這是最推薦的用法,既避免了線程死等,又提供了一定的緩沖。

三、 代碼實戰(zhàn):打造企業(yè)級限流組件

接下來,我來實現(xiàn)一個功能完備的 @RateLimit 組件,支持QPS配置、阻塞/非阻塞模式、超時控制以及預熱模式

1. 引入依賴

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 定義注解@RateLimit

這個注解承載了限流的所有配置元數(shù)據(jù)。

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限流閾值 (QPS),默認每秒 5 個
     */
    double qps() default 5.0;
    /**
     * 獲取令牌的策略
     * true: 阻塞模式(直到拿到令牌或超時)
     * false: 非阻塞模式(拿不到立即失?。?
     */
    boolean block() default true;
    /**
     * 阻塞等待的超時時間(僅當 block=true 時生效)
     * 默認 0,表示無限等待
     */
    long timeout() default 0;
    /**
     * 超時時間單位
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
    /**
     * 預熱時間
     * 默認 0 (SmoothBursty);設(shè)置 >0 則開啟預熱模式 (SmoothWarmingUp)
     */
    long warmupPeriod() default 0;
    /**
     * 預熱時間單位
     */
    TimeUnit warmupUnit() default TimeUnit.SECONDS;
    /**
     * 限流提示信息
     */
    String message() default "系統(tǒng)繁忙,請稍后再試";
}

3. 定義全局異常RateLimitException

public class RateLimitException extends RuntimeException {
    public RateLimitException(String message) {
        super(message);
    }
}

4. 實現(xiàn)切面RateLimitAop

這是限流組件的“大腦”。需要重點關(guān)注實例緩存、線程安全以及不同策略的執(zhí)行邏輯。

import com.google.common.util.concurrent.RateLimiter;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Aspect
@Component
public class RateLimitAop {
    
    // 使用 ConcurrentHashMap 緩存 RateLimiter 實例,確保線程安全
    // Key: 方法簽名 (類名.方法名(參數(shù)類型)), Value: 限流器實例
    private final Map<String, RateLimiter> rateLimiterCache = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.example.annotation.RateLimit)")
    public void rateLimitPointcut() {}

    @Around("rateLimitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RateLimit annotation = method.getAnnotation(RateLimit.class);

        // 1. 構(gòu)建方法唯一 Key,防止方法重載沖突
        String methodKey = buildMethodKey(method);
        
        // 2. 線程安全地創(chuàng)建或獲取限流器
        RateLimiter rateLimiter = rateLimiterCache.computeIfAbsent(methodKey, key -> createRateLimiter(annotation));

        // 3. 執(zhí)行獲取令牌邏輯
        boolean acquireSuccess;
        if (annotation.block()) {
            // --- 阻塞模式 ---
            if (annotation.timeout() <= 0) {
                // 無限等待,直到成功
                rateLimiter.acquire();
                acquireSuccess = true;
            } else {
                // 限時等待
                acquireSuccess = rateLimiter.tryAcquire(annotation.timeout(), annotation.timeUnit());
            }
        } else {
            // --- 非阻塞模式 ---
            // 立即嘗試,失敗即返回
            acquireSuccess = rateLimiter.tryAcquire();
        }

        // 4. 限流攔截
        if (!acquireSuccess) {
            log.warn("【限流報警】方法 {} 請求頻率過高,已拒絕。", methodKey);
            throw new RateLimitException(annotation.message());
        }

        // 5. 放行
        return joinPoint.proceed();
    }

    /**
     * 生成方法簽名:Package.Class.Method(ParamType1,ParamType2)
     */
    private String buildMethodKey(Method method) {
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append(method.getDeclaringClass().getName())
                .append(".").append(method.getName()).append("(");
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++) {
            keyBuilder.append(parameterTypes[i].getSimpleName());
            if (i < parameterTypes.length - 1) {
                keyBuilder.append(",");
            }
        }
        keyBuilder.append(")");
        return keyBuilder.toString();
    }

    /**
     * 工廠方法:根據(jù)配置創(chuàng)建具體的 RateLimiter
     */
    private RateLimiter createRateLimiter(RateLimit annotation) {
        if (annotation.warmupPeriod() > 0) {
            log.info("創(chuàng)建預熱限流器: QPS={}, Warmup={}s", annotation.qps(), annotation.warmupPeriod());
            return RateLimiter.create(annotation.qps(), annotation.warmupPeriod(), annotation.warmupUnit());
        } else {
            log.info("創(chuàng)建標準限流器: QPS={}", annotation.qps());
            return RateLimiter.create(annotation.qps());
        }
    }
}

5. 業(yè)務(wù)接入示例

@Service
public class DataSyncService {

    // 場景1:核心數(shù)據(jù)同步,允許排隊等待500ms,保證盡可能執(zhí)行
    @RateLimit(qps = 10.0, block = true, timeout = 500)
    public void syncImportantData(List<Data> dataList) {
        // ... 業(yè)務(wù)邏輯 ...
    }

    // 場景2:非核心接口,流量大時直接丟棄,保護系統(tǒng)
    @RateLimit(qps = 50.0, block = false, message = "當前訪問人數(shù)過多")
    public void refreshCache() {
        // ... 刷新邏輯 ...
    }
}

四、 進階:聊聊動態(tài)代理那個“大家都知道”的坑

在使用 AOP 時,有一個經(jīng)典面試題級別的現(xiàn)象:類內(nèi)方法自調(diào)用導致 AOP 失效。作為開發(fā)者,我們不止要知其然,更知其所以然。

場景重現(xiàn)

@Service
public class TradeService {
    public void process() {
        // ... 前置處理 ...
        pay(); // ? 重點在這里:直接調(diào)用內(nèi)部方法
    }

    @RateLimit(qps = 5.0) 
    public void pay() { ... }
}

為什么會失效?

Spring AOP 的底層使用的是 動態(tài)代理。

  • 容器啟動時,Spring 為 TradeService 生成了一個代理對象(Proxy)。
  • 外部調(diào)用 process() 時,先走的是代理。
  • 但在 process() 內(nèi)部執(zhí)行 pay() 時,使用的是 this.pay()。這里的 this 指向的是目標對象本身,而非代理對象。
  • 既然沒經(jīng)過代理,切面邏輯自然就像空氣一樣被穿透了。

避坑建議

針對此類問題,我推薦以下處理方式:

推薦:拆分大法(Best Practice)

pay() 方法拆分到另一個獨立的 Bean(例如 PayService)中。通過注入的方式調(diào)用,天然符合“通過代理調(diào)用”的規(guī)則,代碼結(jié)構(gòu)也更清晰。

推薦:AopContext

直接從 Spring 上下文中撈取當前代理對象。(老功能修改)

  1. SpringBoot啟動類上開啟配置:@EnableAspectJAutoProxy(exposeProxy = true)
  2. 具體代碼中修改:((TradeService) AopContext.currentProxy()).pay();

不推薦:@Autowired 注入自身

雖然能解決問題,但容易引發(fā)循環(huán)依賴異常,增加系統(tǒng)啟動風險。

五、 進階思考:從單機到分布式

前面我強調(diào)了 Guava RateLimiter單機限流。那么,如果系統(tǒng)做大了,部署了 50 個節(jié)點,需要對某個下游 API 做全局每秒 1000 次的限流,該怎么辦?

這時候,AOP + 注解 設(shè)計模式的威力就體現(xiàn)出來了。

完全不需要修改任何業(yè)務(wù)代碼,也不用刪掉 @RateLimit 注解。 你只需要做一個動作:修改 RateLimitAop 切面的實現(xiàn)

把切面里獲取令牌的邏輯,從 Guava RateLimiter 換成 Redis + Lua 腳本,或者直接接入 RedissonRRateLimiter。

// 偽代碼示例:無縫切換分布式限流
private RRateLimiter getRedisLimiter(String key) {
    RRateLimiter limiter = redissonClient.getRateLimiter(key);
    // ... 初始化 Redis 限流器 ...
    return limiter;
}

// 在 around 方法里,將 RateLimiter.tryAcquire() 替換為 Redisson 的實現(xiàn)
RRateLimiter limiter = getRedisLimiter(methodKey);
if (!limiter.tryAcquire(annotation.qps(), annotation.timeout(), annotation.timeUnit())) {
    throw new RateLimitException("分布式限流生效中...");
}

看,這就是架構(gòu)設(shè)計的藝術(shù)。業(yè)務(wù)方無感知,底層能力平滑升級。

六、 總結(jié)與結(jié)語

總的來說,AOP 讓限流這類“基礎(chǔ)設(shè)施”悄無聲息地融入了業(yè)務(wù)脈絡(luò),這正是優(yōu)雅架構(gòu)的魅力所在——將復雜性收斂于一點,在別處換來 simplicity。

到此這篇關(guān)于Spring AOP + Guava RateLimiter實現(xiàn)優(yōu)雅限流的文章就介紹到這了,更多相關(guān)Spring AOP Guava RateLimiter限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

三原县| 重庆市| 莲花县| 巨鹿县| 纳雍县| 丽江市| 平罗县| 道真| 五河县| 宜兴市| 太和县| 金坛市| 扶余县| 崇礼县| 皮山县| 乌什县| 陆河县| 苏州市| 永靖县| 五华县| 瓦房店市| 故城县| 南郑县| 武汉市| 定兴县| 永顺县| 新乐市| 连平县| 股票| 宾阳县| 濮阳市| 长汀县| 武功县| 辽阳市| 石渠县| 营口市| 炎陵县| 金塔县| 西平县| 勐海县| 油尖旺区|