寫在前面
提起 AOP(面向切面編程),大家的第一反應往往是:“哦,那個用來打印日誌、管理事務、或者做權限校驗的。”
其實,AOP 的能力遠不止於此。在面對高併發場景下的接口自我保護時,它同樣能發揮奇效。
最近在項目中遇到了一個真實場景:這是一個基於 MQ 觸發的定時跑批任務。平日裏風平浪靜,可是一旦大促或者數據量激增,MQ 裏的積壓消息就會瞬間推送給消費者。
雖然消費者服務雖然處理得過來,但底層的核心業務數據庫卻扛不住了——大量併發查詢瞬間打滿 CPU,CPU 使用率飆升至 100%,直接影響了線上實時業務的穩定性。
考慮到該服務是單節點部署,引入 Redis 做分佈式限流顯得“殺雞用牛刀”,也增加了額外的運維成本。最終,我決定使用 Spring AOP + Guava RateLimiter + 自定義註解,實現一個 無侵入、可配置、輕量級 的單機限流組件。

一、 為什麼選擇 AOP + 註解?
在介紹代碼之前,先明確設計初衷。
以前我剛接觸開發時,也喜歡在 Service 或 Controller 層直接硬編碼限流邏輯,例如:
// ❌ 反例:硬編碼,邏輯混雜且難以複用
if (!rateLimiter.tryAcquire()) {
throw new RuntimeException("系統繁忙");
}
doBusiness();
這種寫法的弊端很明顯:
- 邏輯混雜:清晰的業務代碼中夾雜着非業務的限流判斷。
- 複用性差:如果有十個接口需要限流,就需要重複編寫十次。
- 維護困難:一旦需要調整限流策略(例如升級為分佈式限流),涉及的修改點將非常多。
AOP(面向切面編程) 的核心就是 “解耦” 和 “複用”。
我將限流邏輯封裝為一個獨立的“切面”,配合自定義註解作為“開關”。只需在目標方法上添加一個註解,限流策略隨即生效。後續的維護與升級,也僅需聚焦於切面邏輯本身,無需觸碰任何業務代碼。
二、 Guava RateLimiter 核心原理
我這次選用的核心庫是 Google Guava 的 RateLimiter。它是基於 令牌桶算法(Token Bucket) 實現的。
1. 簡單回顧令牌桶
它的機制不像“漏桶”那樣死板(恆定速率流出),而是更加人性化:
- 生產令牌:系統以固定速率向桶中放入令牌。
- 消費令牌:請求過來時,必須先拿到令牌才能執行。
- 關鍵特性:支持突發流量。如果一段時間沒有請求,桶裏的令牌會積攢起來(直到達到桶上限)。當一波突發流量到來時,可以直接消耗積攢的令牌立刻執行,而不需要排隊等待。
2. 兩種核心模式
Guava 貼心地提供了兩種實現:
- SmoothBursty(平滑突發):默認模式。適合大多數場景,允許短時間的流量突發。
- SmoothWarmingUp(平滑預熱):預熱模式。啓動初期令牌發放速率較慢,隨着時間推移逐步提升到目標 QPS。這對於需要“熱身”的資源(如數據庫連接池、緩存填充)非常友好,防止冷啓動時瞬間被打掛。
3. 單機版警告 ⚠️
注意:Guava RateLimiter 是 單機限流 工具!令牌是存在當前 JVM 內存裏的。
- 如果你的服務只部署一台機器,它完美勝任。
- 如果你部署了 10 台機器,每台設置 QPS=5,那麼整個集羣的總 QPS 上限是 50。
4. 常用 API 詳解
熟練掌握 API 是實戰的基礎,以下是 RateLimiter 的核心方法:
核心創建方法
| 方法簽名 | 説明 |
|---|---|
create(double permitsPerSecond) |
創建 SmoothBursty 限流器,指定每秒生成的令牌數(默認:permitsPerSecond = QPS = 桶容量)。 |
create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) |
創建 SmoothWarmingUp 限流器,指定 QPS + 預熱時間。 |
核心獲取方法
| 方法簽名 | 説明 |
|---|---|
double acquire() |
阻塞式獲取 1 個令牌。若無令牌,線程會一直等待,直到獲取成功。 |
double acquire(int permits) |
阻塞式獲取指定數量的令牌(可一次獲取多個)。 |
boolean tryAcquire() |
非阻塞式獲取 1 個令牌。立即返回:成功 true,失敗 false(不等待)。 |
boolean tryAcquire(long timeout, TimeUnit unit) |
限時等待獲取 1 個令牌。在超時時間內拿到返回 true,否則返回 false。這是最推薦的用法,既避免了線程死等,又提供了一定的緩衝。 |
三、 代碼實戰:打造企業級限流組件
接下來,我來實現一個功能完備的 @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
這個註解承載了限流的所有配置元數據。
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);設置 >0 則開啓預熱模式 (SmoothWarmingUp)
*/
long warmupPeriod() default 0;
/**
* 預熱時間單位
*/
TimeUnit warmupUnit() default TimeUnit.SECONDS;
/**
* 限流提示信息
*/
String message() default "系統繁忙,請稍後再試";
}
3. 定義全局異常 RateLimitException
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
}
4. 實現切面 RateLimitAop
這是限流組件的“大腦”。需要重點關注實例緩存、線程安全以及不同策略的執行邏輯。
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: 方法簽名 (類名.方法名(參數類型)), 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. 構建方法唯一 Key,防止方法重載衝突
String methodKey = buildMethodKey(method);
// 2. 線程安全地創建或獲取限流器
RateLimiter rateLimiter = rateLimiterCache.computeIfAbsent(methodKey, key -> createRateLimiter(annotation));
// 3. 執行獲取令牌邏輯
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();
}
/**
* 工廠方法:根據配置創建具體的 RateLimiter
*/
private RateLimiter createRateLimiter(RateLimit annotation) {
if (annotation.warmupPeriod() > 0) {
log.info("創建預熱限流器: QPS={}, Warmup={}s", annotation.qps(), annotation.warmupPeriod());
return RateLimiter.create(annotation.qps(), annotation.warmupPeriod(), annotation.warmupUnit());
} else {
log.info("創建標準限流器: QPS={}", annotation.qps());
return RateLimiter.create(annotation.qps());
}
}
}
5. 業務接入示例
@Service
public class DataSyncService {
// 場景1:核心數據同步,允許排隊等待500ms,保證儘可能執行
@RateLimit(qps = 10.0, block = true, timeout = 500)
public void syncImportantData(List<Data> dataList) {
// ... 業務邏輯 ...
}
// 場景2:非核心接口,流量大時直接丟棄,保護系統
@RateLimit(qps = 50.0, block = false, message = "當前訪問人數過多")
public void refreshCache() {
// ... 刷新邏輯 ...
}
}
四、 進階:聊聊動態代理那個“大家都知道”的坑
在使用 AOP 時,有一個經典面試題級別的現象:類內方法自調用導致 AOP 失效。作為開發者,我們不止要知其然,更知其所以然。
場景重現
@Service
public class TradeService {
public void process() {
// ... 前置處理 ...
pay(); // ❌ 重點在這裏:直接調用內部方法
}
@RateLimit(qps = 5.0)
public void pay() { ... }
}
為什麼會失效?
Spring AOP 的底層使用的是 動態代理。
- 容器啓動時,Spring 為
TradeService生成了一個代理對象(Proxy)。 - 外部調用
process()時,先走的是代理。 - 但在
process()內部執行pay()時,使用的是this.pay()。這裏的this指向的是目標對象本身,而非代理對象。 - 既然沒經過代理,切面邏輯自然就像空氣一樣被穿透了。
避坑建議
針對此類問題,我推薦以下處理方式:
推薦:拆分大法(Best Practice)
將 pay() 方法拆分到另一個獨立的 Bean(例如 PayService)中。通過注入的方式調用,天然符合“通過代理調用”的規則,代碼結構也更清晰。
推薦:AopContext
直接從 Spring 上下文中撈取當前代理對象。(老功能修改)
- SpringBoot啓動類上開啓配置:
@EnableAspectJAutoProxy(exposeProxy = true) - 具體代碼中修改:
((TradeService) AopContext.currentProxy()).pay();
不推薦:@Autowired 注入自身
雖然能解決問題,但容易引發循環依賴異常,增加系統啓動風險。
五、 進階思考:從單機到分佈式
前面我強調了 Guava RateLimiter 是單機限流。那麼,如果系統做大了,部署了 50 個節點,需要對某個下游 API 做全局每秒 1000 次的限流,該怎麼辦?
這時候,AOP + 註解 設計模式的威力就體現出來了。
你完全不需要修改任何業務代碼,也不用刪掉 @RateLimit 註解。
你只需要做一個動作:修改 RateLimitAop 切面的實現。
把切面裏獲取令牌的邏輯,從 Guava RateLimiter 換成 Redis + Lua 腳本,或者直接接入 Redisson 的 RRateLimiter。
// 偽代碼示例:無縫切換分佈式限流
private RRateLimiter getRedisLimiter(String key) {
RRateLimiter limiter = redissonClient.getRateLimiter(key);
// ... 初始化 Redis 限流器 ...
return limiter;
}
// 在 around 方法裏,將 RateLimiter.tryAcquire() 替換為 Redisson 的實現
RRateLimiter limiter = getRedisLimiter(methodKey);
if (!limiter.tryAcquire(annotation.qps(), annotation.timeout(), annotation.timeUnit())) {
throw new RateLimitException("分佈式限流生效中...");
}
看,這就是架構設計的藝術。業務方無感知,底層能力平滑升級。
六、 總結與結語
總的來説,AOP 讓限流這類“基礎設施”悄無聲息地融入了業務脈絡,這正是優雅架構的魅力所在——將複雜性收斂於一點,在別處換來 simplicity。
最後,想起一句被反覆“魔改”的名言,放在這裏格外貼切:“讓架構的歸架構,讓業務的歸業務”。
願各位的代碼世界,秩序井然,bug 退散。