Spring Boot自定义注解+AOP,使用guava的RateLimiter实现接口的限流

2023-05-16

目录

一、需求

二、设计

漏桶算法

令牌桶算法

几种算法对比

三、相关代码

1. 引入相关依赖

2.自定义注解 @RateLimit

3.封装限流器 EfRateLimiter

4.定义AOP切面

5.在接口中使用@RateLimit来开启限流:


一、需求

        接口限流,支持通过配置文件设置是否开启限流,限流的大小,以及超时时间

二、设计

常用限流算法:漏桶算法、令牌桶算法、滑动窗口(计数器)算法

漏桶算法

        漏桶非常均匀的控制流量,如果漏桶满了,后续的水全部会溢出,用它来作为应用层限流是不合适的。如果有大量的用户访问,会导致后面的用户全部拒绝服务,给人的感觉就像服务挂了一样。

令牌桶算法

        令牌桶算法恰好相反,桶里放的不是请求,而是令牌。当请求到来时,需要从桶中拿到一个令牌才能获取服务,否则该请求会被拒绝。由于令牌桶是动态变化的,令牌消耗完了会继续往里放,因此就不存在漏桶那样后面的用户拿不到令牌的情况,是一个比较平滑的过程。

 

 

几种算法对比

        令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
        令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;
        令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率;

guava的RateLimiter使用的是令牌桶算法,有两种实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)

 

三、相关代码

1. 引入相关依赖

AOP

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

guava 

            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>20.0</version>
            </dependency>

2.自定义注解 @RateLimit


import org.springframework.beans.factory.annotation.Required;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * 自定义注解,用于接口的限流
 *
 * @author lizf
 * date: 2022/7/18 11:53
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限流器名称,如果不设置,默认是类名加方法名。如果多个接口设置了同一个名称,那么使用同一个限流器
     *
     * @return
     */
    String name() default "";

    /**
     * 一秒内允许通过的请求数QPS
     *
     * @return
     */
    @Required
    String permitsPerSecond();

    /**
     * 获取令牌超时时间
     *
     * @return
     */
    String timeout() default "0";

    /**
     * 获取令牌超时时间单位
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

3.封装限流器 EfRateLimiter


import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.TimeUnit;

/**
 * 接口限流器
 *
 * @author lizf
 * date: 2022/7/18 14:20
 */
public class EfRateLimiter {
    private RateLimiter rateLimiter;
    private long timeout;
    private TimeUnit timeUnit;

    public RateLimiter getRateLimiter() {
        return rateLimiter;
    }

    public void setRateLimiter(RateLimiter rateLimiter) {
        this.rateLimiter = rateLimiter;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public TimeUnit getTimeUnit() {
        return timeUnit;
    }

    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }

    public boolean tryAcquire() {
        return rateLimiter.tryAcquire(timeout, timeUnit);
    }

    public boolean tryAcquire(int permits) {
        return rateLimiter.tryAcquire(permits, timeout, timeUnit);
    }

}

4.定义AOP切面

这里使用的切点、通知,采用了增强的方式,可以直接在通知的参数里获取自定义注解里的内容,省却了通过反射来获取注解里的内容。参考链接:

AOP高级特性,Advice Parameters,在拦截方法里配置参数、自定义注解对象等_lzhfdxhxm的博客-CSDN博客


import com.google.common.util.concurrent.RateLimiter;
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.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 接口限流切面
 * 配合@RateLimit使用
 *
 * @author lizf
 * date: 2022/7/18 14:20
 */
@Aspect
@Component
@ConditionalOnProperty(prefix = "rate.limit.default", name = "enabled", havingValue = "true")
public class SmoothBurstyInterceptor implements EnvironmentAware {
    private static final Log log = LogFactory.getLog(SmoothBurstyInterceptor.class);
    private static final Map<String, EfRateLimiter> EF_RATE_LIMITER_MAP = new ConcurrentHashMap<>();
    private Environment environment;

    @Value("${rate.limit.default.permitsPerSecond:1000}")
    private double defaultPermitsPerSecond;

    @Pointcut("@annotation(rateLimit)")
    public void pointCut(RateLimit rateLimit) {
    }

    @Around(value = "pointCut(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        String className = pjp.getTarget().getClass().getSimpleName();
        String methodName = signature.getName();
        String rateLimitName = environment.resolvePlaceholders(rateLimit.name());
        if (EmptyUtil.isEmpty(rateLimitName) || rateLimitName.contains("${")) {
            rateLimitName = className + "-" + methodName;
        }

        EfRateLimiter rateLimiter = this.getRateLimiter(rateLimitName, rateLimit);
        boolean success = rateLimiter.tryAcquire();
        Object[] args = pjp.getArgs();
        if (success) {
            return pjp.proceed(args);
        } else {
            log.error("MQ_HDL > {}.{}(), rate limiting, parameters[{}]", className, methodName, args);
            throw new BizException(EfMessageCode.ERR_INTCPT_RATE_LIMIT, "接口访问太过频繁,请稍候再试");
        }
    }

    private EfRateLimiter getRateLimiter(String key, RateLimit rateLimit) {
        EfRateLimiter efRateLimiter = EF_RATE_LIMITER_MAP.get(key);
        if (efRateLimiter == null) {
            synchronized (this) {
                if ((efRateLimiter = EF_RATE_LIMITER_MAP.get(key)) == null) {
                    String permitsPerSecondStr = environment.resolvePlaceholders(rateLimit.permitsPerSecond());
                    double permitsPerSecond = defaultPermitsPerSecond;
                    if (EmptyUtil.isNotEmpty(permitsPerSecondStr) && !permitsPerSecondStr.contains("${")) {
                        permitsPerSecond = Double.valueOf(permitsPerSecondStr);
                    }
                    efRateLimiter = new EfRateLimiter();
                    RateLimiter rateLimiter = RateLimiter.create(permitsPerSecond);
                    String timeoutStr = environment.resolvePlaceholders(rateLimit.timeout());
                    long timeout = 0L;
                    if (EmptyUtil.isNotEmpty(timeoutStr) && !timeoutStr.contains("${")) {
                        timeout = Math.max(Integer.valueOf(timeoutStr), 0L);
                    }
                    TimeUnit timeUnit = rateLimit.timeUnit();

                    efRateLimiter.setRateLimiter(rateLimiter);
                    efRateLimiter.setTimeout(timeout);
                    efRateLimiter.setTimeUnit(timeUnit);
                    EF_RATE_LIMITER_MAP.putIfAbsent(key, efRateLimiter);
                }
            }
        }

        return efRateLimiter;
    }

    /**
     * Set the {@code Environment} that this component runs in.
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

5.在接口中使用@RateLimit来开启限流:

@Controller
@RequestMapping(value = "/rttp")
public class AdapterController {
    @ApiOperation("外部使用json调用")
    @PostMapping(path = "/service4OutAppJson", consumes = "text/plain", produces = "text/plain")
    @ResponseBody
    @RateLimit(permitsPerSecond = "${rate.limit.adapter.out.permitsPerSecond:}", timeout = "${rate.limit.adapter.out.timeout:}")
    public String service4OutAppJson(@RequestBody String json) {
        Request req = new Request();
        req.setJson(json);
        Response<String> resp = new Response<>();
        Transaction<String> tran = new Transaction<>(req, resp, outAppCodeLocId);
        handler.handle(tran);
        return tran.getResp().getContent();
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Boot自定义注解+AOP,使用guava的RateLimiter实现接口的限流 的相关文章

  • 关系型数据库删除记录之后,如何同步到索引库ElasticSearch

    利用logstash xff0c 可以很方便的全量或增量同步MySql表中的数据 可是如果MySql表里删除了记录 xff0c 就没有办法直接删除对应的索引信息了 变通方法 xff1a 方法一 xff1a 在删除MySql表里的记录的时候
  • nginx/tengine添加模块

    项目中使用的tengine xff0c 在后期说要添加健康检查模块ngx http upstream check module xff0c 没办法 xff0c 只能动态添加 tengine的官方说明 xff1a ngx http upstr
  • Spring参数校验之Bean的分组校验@Validated

    利用好javax validation groups Default class 这个默认分组 一 建立不同的分组 注意建立的是接口 xff0c 继承Default AddGroup public interface AddGroup ex
  • @FeignClient注解的接口,用@Autowired可能获取不到实例

    背景 xff1a Spring Boot 2 0 8 RELEASE Spring Cloud 2 0 4 RELEASE OpenFeign 2 0 4 RELEASE JDK 1 8 启动类 xff1a package com xxx
  • 如何在Ubuntu 20.04 上安装 Xrdp 服务器(远程桌面)

    简介 xff1a Xrdp 是一个微软远程桌面协议 xff08 RDP xff09 的开源实现 xff0c 它允许你通过图形界面控制远程系统 通过 RDP xff0c 你可以登录远程机器 xff0c 并且创建一个真实的桌面会话 xff0c
  • ThreadLocal的脏数据、内存泄露

    背景 xff1a Spring Boot 2 0 8 RELEASE Spring Cloud 2 0 4 RELEASE tomcat 8 5 37 JDK 1 8 项目使用ThreadLocal来传递参数 xff0c 比如 xff1a
  • idea的Database无法提示表名、字段名、无法加载schema问题

    在使用idea的Database的时候 xff0c 数据库已经连接上了 xff0c 查询也能查出数据来 可是表名显示是红色的 xff0c 而且也无法提示 自动填充表名和字段名 xff0c 如下图 xff1a idea的提示是Unable t
  • idea启动项目很慢

    随着项目的开发 调试 xff0c 在一段时间之后 xff0c 发现项目启动的速度越来越慢 xff0c 甚至到了5分钟都没启动完的情况 刚开始以为是项目代码的问题 xff0c 可以问了同事之后 xff0c 他们说项目启动还是很快的 xff0c
  • JVM规范 oracle官网

    Java SE Specifications oracle com 可以选择jdk版本来看对应的JVM规范 如常用的Java SE 8 Edition xff0c class文件的结构说明 xff1a Chapter 4 The class
  • package-info.java的作用

    目录 作用 一 提供包级别的注释 1 在idea里的体现 xff1a 2 在Javadocs里的体现 xff1a 二 提供包级别的注解 三 提供包级别的友好类和变量 1 在同一个包里能正常使用 2 在别的包里就不能使用 xff0c 包括子包
  • 浅复制和深复制-以HashMap为例

    目录 1 简介 2 浅复制和深复制 2 1浅复制 xff08 shallow copy xff09 2 2深复制 xff08 deep copy xff09 3 常见实现方式 3 1 浅复制 3 1 1使用HashMap的构造器 3 1 2
  • 序列化和反序列化

    摘要 这里说的序列化 反序列化是针对数据结构和二进制之间的相互转换 比较常用的序列化协议有 hessian kyro protostuff 序列化和反序列化几乎是工程师们每天都要面对的事情 xff0c 但是要精确掌握这两个概念并不容易 xf
  • Java 获取泛型的类型实例详解

    Java 获取泛型的类型实例详解 Java 泛型实际上有很多缺陷 xff0c 比如不能直接获取泛型的类型 xff0c 不能获取带泛型类等 以下方式是不正确的 获取带泛型的类的类型 1 Class lstUClazz 61 List lt U
  • idea启动项目失败 YAMLException 中文文件编码格式

    目录 环境信息 xff1a 问题描述 xff1a 解决方案 xff1a 解决思路 xff1a 解决方法 xff1a 1 查看application yml文件里是否有中文 2 查看application yml的编码格式 xff0c 是不是
  • idea读取配置文件如ValidationMessages.properties中文乱码

    目录 环境信息 xff1a 问题描述 xff1a 解决方案 xff1a 解决思路 xff1a 解决方法 xff1a 总结 环境信息 xff1a idea2021 1 1 xff08 注 xff1a 如果是其它版本的idea xff0c 也可
  • 【超简单5分钟~最新版】微信公众号早安定时推送 带天气、纪念日、生日、定时推送等(附4.0最新版)

    微信公众号早安推送 无计算机基础 xff0c 5分钟即设置好 xff08 定时推送 及 最新版 在文章末 xff09 效果如图 xff1a 操作步骤 xff1a 1 百度搜索 微信公众平台测试号申请 xff0c 自己扫码登陆 https m
  • SpringBoot项目启动失败,Ambiguous mapping. Cannot map ‘xxxController‘ method

    目录 项目场景 xff1a 问题描述 xff1a 原因分析 xff1a 解决方案 xff1a 项目场景 xff1a SpringBoot 2 1 15 RELEASE 问题描述 xff1a SpringBoot项目启动失败了 xff0c 后
  • idea操作git获取其它分支的文件

    需求 xff1a 代码有多个分支 xff0c 其中一个分支A是专门给一个项目组使用的 xff0c 和master分支差别比较大 在公司开发的时候 xff0c 提供给项目组的代码需要单独对比 合并 xff0c 不能直接从master合并到分支
  • RequestContextHolder分析

    需求 xff1a 在一次请求的过程中 xff0c 想要获取request和response 如果每个方法都需要把这两个当成入参 xff0c 这样显得很不雅观 Spring web包里就提供了RequestContextHolder这个类来方
  • 从request里获取客户端的ip

    目录 代码 xff1a x forwarded for 参考 xff1a 代码 xff1a public static String getIpAddress HttpServletRequest request String ip 61

随机推荐