🌺The Begin🌺点点关注,收藏不迷路🌺

引言:当AOP遇见限流

提起AOP(面向切面编程),大家的第一反应往往是:“哦,那个用来打印日志、管理事务、或者做权限校验的。”

其实,AOP的能力远不止于此。在面对高并发场景下的接口自我保护时,它同样能发挥奇效。

真实场景:一个基于MQ触发的定时跑批任务。平日里风平浪静,可是一旦大促或者数据量激增,MQ里的积压消息就会瞬间推送给消费者。虽然消费者服务处理得过来,但底层的核心业务数据库却扛不住了——大量并发查询瞬间打满CPU,直接影响了线上实时业务的稳定性。

解决方案:Spring AOP + Guava RateLimiter + 自定义注解,实现一个无侵入、可配置、轻量级的单机限流组件。

在这里插入图片描述


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

1.1 硬编码的痛点

以前我刚接触开发时,也喜欢在Service或Controller层直接硬编码限流逻辑:

// ❌ 反例:硬编码,逻辑混杂且难以复用
public String doBusiness() {
    // 限流判断混在业务代码中
    if (!rateLimiter.tryAcquire()) {
        throw new RuntimeException("系统繁忙");
    }
    
    // 真实的业务逻辑
    // ... 几十行代码 ...
    return "success";
}

这种写法的弊端很明显:

问题 说明
逻辑混杂 清晰的业务代码中夹杂着非业务的限流判断
复用性差 十个接口需要限流,就需要重复编写十次
维护困难 调整限流策略,所有涉及点都要修改

1.2 AOP的解决方案

AOP的核心就是**“解耦""复用”**。

// ✅ 正例:注解式限流,业务代码干干净净
@RateLimit(qps = 5.0, block = false)
public String doBusiness() {
    // 只有纯粹的业务逻辑
    // ... 几十行代码 ...
    return "success";
}

二、Guava RateLimiter核心原理 📚

2.1 令牌桶算法

Guava的RateLimiter基于**令牌桶算法(Token Bucket)**实现:

令牌桶

放入令牌

获取令牌

获取令牌

等待令牌

令牌生成器
固定速率

令牌桶

请求1

请求2

请求3

工作机制

  • 生产令牌:系统以固定速率向桶中放入令牌
  • 消费令牌:请求过来时,必须先拿到令牌才能执行
  • 支持突发:闲置时令牌积攒,突发流量可快速消耗

2.2 两种核心模式

模式 特点 适用场景
SmoothBursty 平滑突发,默认模式 大多数业务场景
SmoothWarmingUp 平滑预热,启动慢后变快 需要"热身"的资源(连接池、缓存)

2.3 核心API

创建方法

// 创建SmoothBursty限流器(每秒5个令牌)
RateLimiter bursty = RateLimiter.create(5.0);

// 创建SmoothWarmingUp限流器(预热10秒)
RateLimiter warmingUp = RateLimiter.create(5.0, 10, TimeUnit.SECONDS);

获取方法

方法 特点 适用场景
acquire() 阻塞式,无令牌则等待 必须执行的任务
tryAcquire() 非阻塞,立即返回 可丢弃的请求
tryAcquire(timeout, unit) 限时等待 最推荐,平衡体验

三、代码实战:打造企业级限流组件 💻

3.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>

3.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.3 定义限流异常

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

3.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: 方法签名 (类名.方法名(参数类型))
    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());
        }
    }
}

3.5 业务接入示例

@Service
public class DataSyncService {

    // 场景1:核心数据同步,允许排队等待500ms,保证尽可能执行
    @RateLimit(qps = 10.0, block = true, timeout = 500)
    public void syncImportantData(List<Data> dataList) {
        // 核心业务逻辑
        System.out.println("同步重要数据: " + dataList.size() + "条");
    }

    // 场景2:非核心接口,流量大时直接丢弃,保护系统
    @RateLimit(qps = 50.0, block = false, message = "当前访问人数过多")
    public void refreshCache() {
        // 非核心业务逻辑
        System.out.println("刷新缓存");
    }
    
    // 场景3:需要预热的数据库连接池操作
    @RateLimit(qps = 100.0, warmupPeriod = 10, warmupUnit = TimeUnit.SECONDS)
    public void queryDatabase() {
        // 数据库查询,需要预热避免冷启动冲击
    }
}

四、进阶:动态代理的那个"坑" ⚠️

4.1 场景重现

@Service
public class TradeService {
    
    public void process() {
        // ... 前置处理 ...
        pay(); // ❌ 重点在这里:直接调用内部方法
    }

    @RateLimit(qps = 5.0) 
    public void pay() {
        System.out.println("执行支付");
    }
}

为什么会失效?

Spring容器

外部调用

内部调用this.pay

❌ 不走代理

代理对象

process方法

目标对象

pay方法

无限流

4.2 解决方案

方案1:拆分大法(推荐)
@Service
public class TradeService {
    
    @Autowired
    private PayService payService; // 注入独立的Bean
    
    public void process() {
        // ... 前置处理 ...
        payService.pay(); // ✅ 通过代理调用
    }
}

@Service
public class PayService {
    
    @RateLimit(qps = 5.0) 
    public void pay() {
        System.out.println("执行支付");
    }
}
方案2:AopContext
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 开启暴露代理
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Service
public class TradeService {
    
    public void process() {
        // ... 前置处理 ...
        // 通过AopContext获取当前代理对象
        ((TradeService) AopContext.currentProxy()).pay(); // ✅ 通过代理调用
    }

    @RateLimit(qps = 5.0) 
    public void pay() {
        System.out.println("执行支付");
    }
}
方案3:注入自身(不推荐)
@Service
public class TradeService {
    
    @Autowired
    private TradeService self; // 注入自身,可能导致循环依赖
    
    public void process() {
        // ... 前置处理 ...
        self.pay(); // ✅ 通过代理调用
    }

    @RateLimit(qps = 5.0) 
    public void pay() {
        System.out.println("执行支付");
    }
}

五、进阶思考:从单机到分布式 🌐

5.1 架构升级的优雅性

当系统从单节点扩展到50个节点,需要对某个下游API做全局每秒1000次的限流时:

在这里插入图片描述

5.2 无缝切换分布式限流

@Slf4j
@Aspect
@Component
public class RateLimitAop {
    
    @Autowired
    private RedissonClient redissonClient; // 注入Redisson
    
    @Around("rateLimitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RateLimit annotation = method.getAnnotation(RateLimit.class);
        
        String methodKey = buildMethodKey(method);
        
        // 替换为分布式限流器
        RRateLimiter limiter = redissonClient.getRateLimiter(methodKey);
        
        // 初始化(只在第一次时设置)
        if (!limiter.isExists()) {
            limiter.trySetRate(RateType.OVERALL, 
                (long)annotation.qps(), 1, RateIntervalUnit.SECONDS);
        }
        
        // 获取令牌
        boolean acquired = limiter.tryAcquire(
            annotation.timeout(), 
            annotation.timeUnit()
        );
        
        if (!acquired) {
            throw new RateLimitException(annotation.message());
        }
        
        return joinPoint.proceed();
    }
}

业务代码无需任何修改,这就是架构设计的艺术!


六、总结与最佳实践 📝

6.1 核心优势

优势 说明
无侵入 业务代码干干净净,专注核心逻辑
可配置 注解参数灵活调整,满足不同场景
轻量级 无需额外中间件,单机部署友好
可扩展 可无缝升级为分布式限流

6.2 最佳实践

// 核心接口:允许短暂排队,提高成功率
@RateLimit(qps = 10.0, block = true, timeout = 500)

// 非核心接口:快速失败,保护系统
@RateLimit(qps = 50.0, block = false)

// 需要预热的资源:平滑启动
@RateLimit(qps = 100.0, warmupPeriod = 10)

// 分布式限流(升级后):业务代码不变
@RateLimit(qps = 1000.0, timeout = 100)

6.3 注意事项

  1. 单机限流不适用于多节点集群,需要分布式方案请升级
  2. 方法自调用会导致AOP失效,注意使用拆分或AopContext
  3. 合理设置超时时间,避免无限等待导致线程堆积
  4. 监控限流日志,及时发现系统瓶颈

总结:优雅架构的魅力

让架构的归架构,让业务的归业务

AOP让限流这类"基础设施"悄无声息地融入了业务脉络,这正是优雅架构的魅力所在——将复杂性收敛于一点,在别处换来simplicity。

// 最终效果:一行注解,万千安心
@RateLimit(qps = 1000)
public Result handleRequest(Request request) {
    // 只有纯粹的业务逻辑
    return doBusiness(request);
}

愿各位的代码世界,秩序井然,bug 退散! 🚀


(本文为Spring Boot实战系列文章,欢迎关注更多企业级开发深度内容)

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐