找回密码
 立即注册
首页 业界区 业界 Spring AOP + Guava RateLimiter:我是如何用注解实现优 ...

Spring AOP + Guava RateLimiter:我是如何用注解实现优雅限流的?

热琢 4 天前
写在前面
提起 AOP(面向切面编程),大家的第一反应往往是:“哦,那个用来打印日志、管理事务、或者做权限校验的。”
其实,AOP 的能力远不止于此。在面对高并发场景下的接口自我保护时,它同样能发挥奇效。
最近在项目中遇到了一个真实场景:这是一个基于 MQ 触发的定时跑批任务。平日里风平浪静,可是一旦大促或者数据量激增,MQ 里的积压消息就会瞬间推送给消费者。
虽然消费者服务虽然处理得过来,但底层的核心业务数据库却扛不住了——大量并发查询瞬间打满 CPU,CPU 使用率飙升至 100%,直接影响了线上实时业务的稳定性。
考虑到该服务是单节点部署,引入 Redis 做分布式限流显得“杀鸡用牛刀”,也增加了额外的运维成本。最终,我决定使用 Spring AOP + Guava RateLimiter + 自定义注解,实现一个 无侵入、可配置、轻量级单机限流组件
1.png

一、 为什么选择 AOP + 注解?

在介绍代码之前,先明确设计初衷。
以前我刚接触开发时,也喜欢在 Service 或 Controller 层直接硬编码限流逻辑,例如:
  1. // ❌ 反例:硬编码,逻辑混杂且难以复用
  2. if (!rateLimiter.tryAcquire()) {
  3.     throw new RuntimeException("系统繁忙");
  4. }
  5. 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. 引入依赖
  1. <dependency>
  2.     <groupId>com.google.guava</groupId>
  3.     guava</artifactId>
  4.     <version>32.1.3-jre</version>
  5. </dependency>
  6. <dependency>
  7.     <groupId>org.springframework.boot</groupId>
  8.     spring-boot-starter-aop</artifactId>
  9. </dependency>
复制代码
2. 定义注解 @RateLimit

这个注解承载了限流的所有配置元数据。
  1. import java.lang.annotation.*;
  2. import java.util.concurrent.TimeUnit;
  3. @Target({ElementType.METHOD})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface RateLimit {
  7.     /**
  8.      * 限流阈值 (QPS),默认每秒 5 个
  9.      */
  10.     double qps() default 5.0;
  11.     /**
  12.      * 获取令牌的策略
  13.      * true: 阻塞模式(直到拿到令牌或超时)
  14.      * false: 非阻塞模式(拿不到立即失败)
  15.      */
  16.     boolean block() default true;
  17.     /**
  18.      * 阻塞等待的超时时间(仅当 block=true 时生效)
  19.      * 默认 0,表示无限等待
  20.      */
  21.     long timeout() default 0;
  22.     /**
  23.      * 超时时间单位
  24.      */
  25.     TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  26.     /**
  27.      * 预热时间
  28.      * 默认 0 (SmoothBursty);设置 >0 则开启预热模式 (SmoothWarmingUp)
  29.      */
  30.     long warmupPeriod() default 0;
  31.     /**
  32.      * 预热时间单位
  33.      */
  34.     TimeUnit warmupUnit() default TimeUnit.SECONDS;
  35.     /**
  36.      * 限流提示信息
  37.      */
  38.     String message() default "系统繁忙,请稍后再试";
  39. }
复制代码
3. 定义全局异常 RateLimitException
  1. public class RateLimitException extends RuntimeException {
  2.     public RateLimitException(String message) {
  3.         super(message);
  4.     }
  5. }
复制代码
4. 实现切面 RateLimitAop

这是限流组件的“大脑”。需要重点关注实例缓存、线程安全以及不同策略的执行逻辑。
[code]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@Componentpublic class RateLimitAop {        // 使用 ConcurrentHashMap 缓存 RateLimiter 实例,确保线程安全    // Key: 方法签名 (类名.方法名(参数类型)), Value: 限流器实例    private final Map 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()

相关推荐

您需要登录后才可以回帖 登录 | 立即注册