SpringBoot基于RateLimiter+AOP动态的为不同接口限流(转载)

博客介绍了限流实现,使用guava的RateLimiter基于令牌桶算法,能简单完成限流并可调整生成token速率。还提及依赖包、自定义注解、封装返回结果、aop实现等内容,最后进行了限流测试,并给出源地址。

限流实现

RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。

依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
 
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

自定义注解

    @Inherited
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimit {
        double limitNum() default 20;  //默认每秒放入桶中的token
    }

封装定义返回结果

    public class MyResult {
        private Integer status;
        private String msg;
        private List<Object> data;
 
        public MyResult(Integer status, String msg, List<Object> data) {
            this.status = status;
            this.msg = msg;
            this.data = data;
        }
 
        public static MyResult OK(String msg, List<Object> data) {
            return new MyResult(200, msg, data);
        }
 
        public static MyResult Error(Integer status, String msg) {
            return new MyResult(status, msg, null);
        }

 aop实现

@Component
@Scope
@Aspect
public class RateLimitAspect {
    private Logger log = LoggerFactory.getLogger(this.getClass());
    //用来存放不同接口的RateLimiter(key为接口名称,value为RateLimiter)
    private ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();
 
    private static ObjectMapper objectMapper = new ObjectMapper();
 
    private RateLimiter rateLimiter;
 
    @Autowired
    private HttpServletResponse response;
 
    @Pointcut("@annotation(com.icat.retalimitaop.annotation.RateLimit)")
    public void serviceLimit() {
    }
 
    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        Object obj = null;
        //获取拦截的方法名
        Signature sig = joinPoint.getSignature();
        //获取拦截的方法名
        MethodSignature msig = (MethodSignature) sig;
        //返回被织入增加处理目标对象
        Object target = joinPoint.getTarget();
        //为了获取注解信息
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        //获取注解信息
        RateLimit annotation = currentMethod.getAnnotation(RateLimit.class);
        double limitNum = annotation.limitNum(); //获取注解每秒加入桶中的token
        String functionName = msig.getName(); // 注解所在方法名区分不同的限流策略
 
        //获取rateLimiter
         if(map.containsKey(functionName)){
             rateLimiter = map.get(functionName);
         }else {
             map.put(functionName, RateLimiter.create(limitNum));
             rateLimiter = map.get(functionName);
         }
 
        try {
            if (rateLimiter.tryAcquire()) {
                //执行方法
                obj = joinPoint.proceed();
            } else {
                //拒绝了请求(服务降级)
                String result = objectMapper.writeValueAsString(MyResult.Error(500, "系统繁忙!"));
                log.info("拒绝了请求:" + result);
                outErrorResult(result);
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return obj;
    }
    //将结果返回
    public void outErrorResult(String result) {
        response.setContentType("application/json;charset=UTF-8");
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            outputStream.write(result.getBytes("utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    static {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }
 
}

测试限流

   2个接口设定没秒限流5个和美妙限流10个 
    @RateLimit(limitNum = 5.0)
    public MyResult getResults() {
        log.info("调用了方法getResults");
        return MyResult.OK("调用了方法", null);
    }
 
    @RateLimit(limitNum = 10.0)
    public MyResult getResultTwo() {
        log.info("调用了方法getResultTwo");
        return MyResult.OK("调用了方法getResultTwo", null);
   }

源地址:https://blog.youkuaiyun.com/qq_39816039/article/details/83988517

<think>首先,用户的问题是:“我想使用Spring Boot AOP结合Redis实现令牌桶限流机制 请问Spring Boot AOP Redis 令牌桶限流 实现方法”。这是一个关于技术实现的问题,涉及Spring Boot、AOP、Redis和令牌桶算法。 我需要参考提供的引用内容: - 引用[1]:提到使用自定义注解和Redis+Lua脚本实现接口限流,但没有具体算法。 - 引用[2]:提到Spring Boot内Redis实现的令牌桶算法限流使用了Redis + Lua脚本,并参考了Spring Gateway的限流算法。 - 引用[3]:提到基于Redis限流使用AOP切面+Redis实现“固定窗口算法”,但作者写错了,应该是令牌桶算法。它描述了令牌桶算法的基本概念:在一定时间内生成令牌,访问时获取令牌,如果没有则拒绝。 关键点: - 令牌桶算法:一种流量控制算法,桶中存储令牌,访问时消耗令牌,桶以固定速率生成令牌。 - 实现:使用Spring Boot AOP来处理方法调用,结合Redis存储令牌桶状态。 - 技术:Redis用于存储和操作令牌桶(因为Redis是内存数据库,适合高并发),Lua脚本确保操作的原子性(避免并发问题)。 - AOP:用于在方法执行前进行限流检查。 我的回答需要: - 结构清晰,逐步解决问题。 - 使用中文。 - 遵循LaTeX格式:行内数学用$...$,独立公式用$$...$$。 - 生成相关问题。 - 在回答中引用段落末尾添加引用标识。 实现步骤: 1. **理解令牌桶算法**: - 桶有最大容量(capacity)。 - 以固定速率(rate)生成令牌。 - 当请求到来时,如果桶中有足够令牌,则移除令牌并允许请求;否则,拒绝。 2. **技术组件**: - **Spring Boot AOP**:定义切面(Aspect),在方法执行前拦截,进行限流检查。 - **Redis**:存储每个接口的令牌桶状态(如当前令牌数、最后更新时间)。使用Lua脚本确保原子操作。 - **自定义注解**:用于标记需要限流的方法,指定限流参数(如rate和capacity)。 3. **实现方法**: - 创建自定义注解,如`@RateLimiter`。 - 实现AOP切面,使用@Around advice。 - 在切面中,使用Redis操作令牌桶: - 获取当前时间和令牌数。 - 计算新令牌数(基于时间差和速率)。 - 如果请求令牌(通常为1)可用,则允许执行;否则,拒绝。 - 使用Lua脚本执行Redis操作,确保原子性。 4. **Lua脚本**: - 参考Spring Gateway的令牌桶算法。 - 脚本示例:计算新令牌数,更新桶状态,返回是否允许请求。 5. **代码示例**: - 提供简化的Java代码片段,展示注解、切面和Redis操作。 6. **生成相关问题**: - 在回答结束时,基于主题生成2-3个相关问题。 回答结构: 1. **引言**:解释令牌桶限流和实现方法。 2. **步骤详解**: a. 创建自定义注解。 b. 实现AOP切面。 c. Redis操作和Lua脚本。 d. 错误处理(如拒绝请求时返回响应)。 3. **代码示例**:提供关键部分代码。 4. **总结**。 5. **相关问题**。 确保LaTeX:如果提到数学公式,如令牌计算: - 行内:$newTokens = \min(capacity, currentTokens + (currentTime - lastUpdateTime) \times rate)$ - 独立公式如果需要。 引用标识:在相关段落末尾添加[^1], [^2], [^3],基于提供的引用。 最终输出: - 用户可见层:回答内容。 - 系统级:相关问题。</think>### Spring Boot AOP结合Redis实现令牌桶限流机制 令牌桶限流是一种高效的流量控制算法,通过维护一个令牌桶(容量为$C$,令牌生成速率为$R$ 令牌/秒)来控制请求速率。当请求到达时,如果桶中有足够令牌(通常消耗1个令牌),则允许执行;否则拒绝请求。这种方法能平滑突发流量,避免系统过载。下面逐步介绍使用Spring Boot AOP、Redis和Lua脚本的实现方法,确保原子性和高并发性能[^2]。 --- ### 实现步骤 #### 1. 添加依赖 在`pom.xml`中添加Spring Boot、AOP和Redis依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` #### 2. 创建自定义注解 定义`@RateLimiter`注解,用于标记需要限流的方法,指定桶容量($C$)和令牌生成速率($R$): ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { String key(); // Redis键前缀,区分不同接口 int capacity(); // 桶最大容量 (C) int rate(); // 令牌生成速率 (R),单位:令牌/秒 } ``` #### 3. 实现AOP切面 创建切面类,使用`@Around`通知拦截被`@RateLimiter`注解的方法。核心逻辑: - **获取注解参数**:读取`capacity`和`rate`。 - **调用Lua脚本**:通过Redis执行原子操作,计算当前可用令牌数。 - **决策**:如果令牌充足,允许方法执行;否则拒绝请求并返回错误响应。 ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; @Aspect @Component public class RateLimiterAspect { @Autowired private RedisTemplate<String, Object> redisTemplate; // Lua脚本(令牌桶算法核心) private static final String LUA_SCRIPT = "local key = KEYS[1] " + "local capacity = tonumber(ARGV[1]) " + "local rate = tonumber(ARGV[2]) " + "local now = tonumber(ARGV[3]) " + "local requested = 1 " + // 每次请求消耗1个令牌 "local lastUpdate = redis.call('get', key..':last') or now " + "local tokens = tonumber(redis.call('get', key..':tokens') or capacity) " + "local elapsed = now - lastUpdate " + "local newTokens = math.min(capacity, tokens + elapsed * rate) " + // $ \text{newTokens} = \min(C, \text{tokens} + (\text{now} - \text{lastUpdate}) \times R) $ "if newTokens >= requested then " + " redis.call('set', key..':tokens', newTokens - requested) " + " redis.call('set', key..':last', now) " + " return 1 " + // 允许请求 "else " + " return 0 " + // 拒绝请求 "end"; @Around("@annotation(rateLimiter)") public Object limit(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { String key = rateLimiter.key(); int capacity = rateLimiter.capacity(); int rate = rateLimiter.rate(); long now = System.currentTimeMillis() / 1000; // 当前时间戳(秒) // 执行Lua脚本(确保原子性) DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(key), capacity, rate, now); if (result == 1) { return joinPoint.proceed(); // 令牌充足,执行原方法 } else { throw new RuntimeException("请求过快,请稍后重试!"); // 拒绝请求 } } } ``` #### 4. Lua脚本详解 - **原子操作**:Lua脚本在Redis中单线程执行,避免并发问题[^2]。 - **令牌计算**:基于时间差($\Delta t = \text{now} - \text{lastUpdate}$)和速率($R$)计算新令牌数: $$ \text{newTokens} = \min(C, \text{tokens} + \Delta t \times R) $$ - **存储设计**:每个接口使用两个Redis键(如`key:tokens`存储当前令牌数,`key:last`存储最后更新时间)。 #### 5. 在Controller中使用注解 在需要限流的方法上添加`@RateLimiter`注解: ```java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ApiController { @GetMapping("/api/resource") @RateLimiter(key = "api_resource", capacity = 10, rate = 2) // 桶容量10,速率2令牌/秒 public String getResource() { return "访问成功!"; } } ``` --- ### 关键优化点 1. **Redis配置**:使用Redis连接池提高性能,确保`spring.redis.*`配置正确。 2. **错误处理**:在切面中添加自定义异常处理,返回HTTP 429(Too Many Requests)[^1]。 3. **动态参数**:通过SpEL表达式支持动态限流参数(如从配置中心读取)。 4. **性能监控**:集成Micrometer监控限流指标,便于优化$C$和$R$值[^2]。 此方法结合了AOP的灵活性和Redis的高并发能力,适合API网关、微服务接口等场景,能有效防止流量洪峰[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值