Spring AOP + Guava RateLimiter:我是如何用注解实现优雅限流的?
写在前面
提起 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 退散。

总的来说,AOP 让限流这类“基础设施”悄无声息地融入了业务脉络,这正是优雅架构的魅力所在——将复杂性收敛于一点,在别处换来 simplicity。
最后,想起一句被反复“魔改”的名言,放在这里格外贴切:“让架构的归架构,让业务的归业务”。
愿各位的代码世界,秩序井然,bug 退散。
浙公网安备 33010602011771号