资讯详情

SpringBoot 如何进行限流

如何优雅地限制流量(基于AOP)。

首先,让我们来看看为什么需要限制接口的流量。

为什么要限流? 由于互联网系统通常面临大并发大流量的要求,在紧急情况下(最常见的场景是第二次杀戮和抢购),即时大流量将直接打破系统,无法提供外部服务。为了防止这种情况,最常见的解决方案之一是限制流量。当要求达到一定的并发数或速度时,等待、排队、降级、拒绝服务等。

比如12306购票系统,面对高并发,采用限流。 提示语经常出现在流量高峰期;目前排队人数较多,请稍后再试!

什么是限流?限流算法有哪些? 限流是限制窗口中的请求数,保持系统的可用性和稳定性,防止系统因流量激增而运行缓慢或停机。

有三种常见的限流算法:

  1. 计数器限流

计数器限流算法是最简单、最粗糙的解决方案,主要用于限制总并发数,如数据库连接池大小、线程池大小、接口访问并发数等。

如:使用 AomicInteger 统计目前并发执行的次数,如果超过域值,直接拒绝要求,提示系统繁忙。

  1. 漏桶算法

漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

  1. 令牌桶算法

令牌桶算法的原理也比较简单,我们可以理解为医院挂号看病,只有拿到号后才能诊病。

该系统将维护令牌(token)桶,以恒定的速度将令牌放入桶中(token),这个时候,如果要求进来处理,需要从桶里拿到令牌(token),桶里没有令牌(token)如果可取,请求将被拒绝。令牌桶算法通过控制桶的容量和发放令牌的速度来限制请求。

基于Guava实现工具类限流 Google开源工具包Guava提供限流工具RateLimiter,基于令牌桶算法实现流量限制,使用非常方便高效,步骤如下:

第一步:引入guava依赖包

<dependency>     <groupId>com.google.guava</groupId>     <artifactId>guava</artifactId>     <version>30.1-jre</version> </dependency> 

第二步:在界面上添加限流逻辑

@Slf4j @RestController @RequestMapping("/limit") public class LimitController {     /**      * 限流策略 : 1秒钟2个请求      */     private final RateLimiter limiter = RateLimiter.create(2.0);      private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");      @GetMapping("/test1")     public String testLimiter() {         //500毫秒内,未取得令牌,直接进入服务降级         boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);          if (!tryAcquire) {             log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf));             return "目前排队人数较多,请稍后再试!";         }          log.info("成功获得令牌,时间{}", LocalDateTime.now().format(dtf));         return "请求成功";     } } 

以上用到了RateLimiter两个核心方法:create()、tryAcquire()以下是详细说明

acquire() 获得令牌, 在获得这个令牌之前,改变方法会被堵塞, 返回值需要时间才能获得这个令牌 acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获得此 N 一个令牌要花时间 tryAcquire() 判断时可获得令牌, 如果没有,立即返回 false tryAcquire(int permits) 获取指定数量的令牌, 如果没有,立即返回 false tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获得令牌, 如果没有,立即返回 false tryAcquire(int permits, long timeout, TimeUnit unit) 同上 第三步:体验效果 访问测试地址: http://127.0.0.1:8080/limit/test1.反复刷新和观察后端日志

WARN LimitController:35 - 2021-09-25进入服务降级 21:39:37 WARN LimitController:35 - 2021-09-25进入服务降级 21:39:37 INFO LimitController:39 - 2021-09-25获得令牌成功 21:39:37 WARN LimitController:35 - 2021-09-25进入服务降级 21:39:37 WARN LimitController:35 - 2021-09-25进入服务降级 21:39:37 INFO LimitController:39 - 2021-09-25获得令牌成功 21:39:37

WARN LimitController:35 - 2021-09-25进入服务降级 21:39:38 INFO LimitController:39 - 2021-09-25获得令牌成功 21:39:38 WARN LimitController:35 - 2021-09-25进入服务降级 21:39:38 INFO LimitController:39 - 2021-09-25获得令牌成功 21:39:38

从上面的日志可以看出,一秒钟内只有两次成功,其他的都失败了,这表明我们已经成功地为界面增加了限流功能。

当然,我们不能直接在实际开发中使用它。至于原因,你想,你需要手动添加每个接口tryAcquire(),业务代码和限流代码混在一起,明显违反DRY原则,代码冗余,重复劳动。代码评审肯定会被老鸟嘲笑,什么破东西!

所以,我们这里需要想办法将其优化 - 借助自定义注释 AOP实现界面限流。

基于AOP实现界面限流 基于AOP实现方法也很简单,实现过程如下:

第一步:加入AOP依赖

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

第二步:自定义限流注释

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit {     /**      * 资源的key,唯一      * 功能:接口不同,不同的流量控制      */     String key() default "";      /**      * 访问限制最多      */     double permitsPerSecond () ;      /**      * 获得令牌最大等待时间      */     long timeout();      /**      * 获得令牌最大等待时间,单位(例:分钟/秒/m秒) 默认:毫秒      */     TimeUnit timeunit() default TimeUnit.MILLISECONDS;      /**      * 没有令牌的提示      */     String msg() default "系统繁忙,请稍后再试."; } 

第三步:使用AOP截面拦截限流注解

@Slf4j @Aspect @Component public class LimitAop {     /**      * 不同的接口,不同的流量控制      * map的key为 Limiter.key      */     private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();      @Around("@annotation(com.jianzh5.blog.limit.Limit)")     public Object around(ProceedingJoinPoint joinPoint) throws Throwable{         MethodSignature signature = (MethodSignature) joinPoint.etSignature();
        Method method = signature.getMethod();
        //拿limit的注解
        Limit limit = method.getAnnotation(Limit.class);
        if (limit != null) {
            //key作用:不同的接口,不同的流量控制
            String key=limit.key();
            RateLimiter rateLimiter = null;
            //验证缓存是否有命中key
            if (!limitMap.containsKey(key)) {
                // 创建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(key, rateLimiter);
                log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(key);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回异常提示
            if (!acquire) {
                log.debug("令牌桶={},获取令牌失败",key);
                this.responseFail(limit.msg());
                return null;
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 直接向前端抛出异常
     * @param msg 提示信息
     */
    private void responseFail(String msg)  {
        HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg);
        WebUtils.writeJson(response,resultData);
    }
}

第四步:给需要限流的接口加上注解

@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {
    
    @GetMapping("/test2")
    @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
    public String limit2() {
        log.info("令牌桶limit2获取令牌成功");
        return "ok";
    }


    @GetMapping("/test3")
    @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")
    public String limit3() {
        log.info("令牌桶limit3获取令牌成功");
        return "ok";
    }
}

第五步:体验效果 通过访问测试地址: http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:

正常响应时:

{“status”:100,“message”:“操作成功”,“data”:“ok”,“timestamp”:1632579377104} 1 触发限流时:

{“status”:2001,“message”:“系统繁忙,请稍后再试!”,“data”:null,“timestamp”:1632579332177} 1 通过观察得之,基于自定义注解同样实现了接口限流的效果。

小结 一般在系统上线时我们通过对系统压测可以评估出系统的性能阀值,然后给接口加上合理的限流参数,防止出现大流量请求时直接压垮系统。今天我们介绍了几种常见的限流算法(重点关注令牌桶算法),基于Guava工具类实现了接口限流并利用AOP完成了对限流代码的优化。

在完成优化后业务代码和限流代码解耦,开发人员只要一个注解,不用关心限流的实现逻辑,而且减少了代码冗余大大提高了代码可读性 ———————————————— 版权声明:本文为CSDN博主「CodingSir」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/educast/article/details/120747339

标签: 继电器底座dtf08a

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台