⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7588376012121669659 「一旅人」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

写在前面

提起 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
publicclass RateLimitAop {

// 使用 ConcurrentHashMap 缓存 RateLimiter 实例,确保线程安全
// Key: 方法签名 (类名.方法名(参数类型)), Value: 限流器实例
privatefinal 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);
thrownew 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
publicclass 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 退散。

文章目录
  1. 1. 写在前面
  2. 2. 一、 为什么选择 AOP + 注解?
  3. 3. 二、 Guava RateLimiter 核心原理
    1. 3.0.1. 1. 简单回顾令牌桶
    2. 3.0.2. 2. 两种核心模式
    3. 3.0.3. 3. 单机版警告 ⚠️
    4. 3.0.4. 4. 常用 API 详解
  • 4. 三、 代码实战:打造企业级限流组件
    1. 4.0.1. 1. 引入依赖
    2. 4.0.2. 2. 定义注解 @RateLimit
    3. 4.0.3. 3. 定义全局异常 RateLimitException
    4. 4.0.4. 4. 实现切面 RateLimitAop
    5. 4.0.5. 5. 业务接入示例
  • 5. 四、 进阶:聊聊动态代理那个“大家都知道”的坑
  • 6. 五、 进阶思考:从单机到分布式
  • 7. 六、 总结与结语