zoukankan      html  css  js  c++  java
  • Guava-RateLimiter实现令牌桶控制接口限流方案

    一.前言

      对于一个应用系统来说,我们有时会遇到极限并发的情况,即有一个TPS/QPS阀值,如果超了阀值可能会导致服务器崩溃宕机,因此我们最好进行过载保护,防止大量请求涌入击垮系统。对服务接口进行限流可以达到保护系统的效果,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

    二.常见限流方案

      1.计数器法

        原理:在单位时间段内,对请求数进行计数,如果数量超过了单位时间的限制,则执行限流策略,当单位时间结束后,计数器清零,这个过程周而复始,就是计数器法。

        缺点:不能均衡限流,在一个单位时间的末尾和下一个单位时间的开始,很可能会有两个访问的峰值,导致系统崩溃。

        改进方式:可以通过减小单位时间来提高精度。

      2.漏桶算法

        原理:假设有一个水桶,水桶有一定的容量,所有请求不论速度都会注入到水桶中,然后水桶以一个恒定的速度向外将请求放出,当水桶满了的时候,新的请求被丢弃。

        优点:可以平滑请求,削减峰值。

        缺点:瓶颈会在漏出的速度,可能会拖慢整个系统,且不能有效地利用系统的资源。  

        

      3.令牌桶算法(推荐)

        原理:有一个令牌桶,单位时间内令牌会以恒定的数量(即令牌的加入速度)加入到令牌桶中,所有请求都需要获取令牌才可正常访问。当令牌桶中没有令牌可取的时候,则拒绝请求。

        优点:相比漏桶算法,令牌桶算法允许一定的突发流量,但是又不会让突发流量超过我们给定的限制(单位时间窗口内的令牌数)。即限制了我们所说的 QPS(每秒查询率)。

        

       

      漏桶算法VS令牌桶算法 

    • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
    • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
    • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
    • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
    • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
    • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

    三.Guava RateLimiter实现平滑限流

      Google开源工具包Guava提供了限流工具类RateLimiter,基于令牌桶算法实现。

      常用方法:

        create(Double permitsPerSecond)方法根据给定的(令牌:单位时间(1s))比例为令牌生成速率
        tryAcquire()方法尝试获取一个令牌,立即返回true/false,不阻塞,重载方法具备设置获取令牌个数、获取最大等待时间等参数
        acquire()方法与tryAcquire类似,但是会阻塞,尝试获取一个令牌,没有时则阻塞直到获取成功

    四.SpringBoot + Interceptor + 自定义注解应用

      1.maven依赖

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

      2.自定义注解

     1 import java.lang.annotation.*;
     2 import java.util.concurrent.TimeUnit;
     3 
     4 /**
     5  * RequestLimiter 自定义注解接口限流
     6  *
     7  * @author xhq
     8  * @version 1.0
     9  * @date 2019/10/22 16:49
    10  */
    11 @Target({ElementType.METHOD})
    12 @Retention(RetentionPolicy.RUNTIME)
    13 @Documented
    14 public @interface RequestLimiter {
    15 
    16     /**
    17      * 每秒创建令牌个数,默认:10
    18      */
    19     double QPS() default 10D;
    20 
    21     /**
    22      * 获取令牌等待超时时间 默认:500
    23      */
    24     long timeout() default 500;
    25 
    26     /**
    27      * 超时时间单位 默认:毫秒
    28      */
    29     TimeUnit timeunit() default TimeUnit.MILLISECONDS;
    30 
    31     /**
    32      * 无法获取令牌返回提示信息
    33      */
    34     String msg() default "亲,服务器快被挤爆了,请稍后再试!";
    35 }

      3.拦截器

     1 import com.google.common.util.concurrent.RateLimiter;
     2 import com.mowanka.framework.annotation.RequestLimiter;
     3 import com.mowanka.framework.web.result.GenericResult;
     4 import com.mowanka.framework.web.result.StateCode;
     5 import org.springframework.stereotype.Component;
     6 import org.springframework.web.method.HandlerMethod;
     7 
     8 import javax.servlet.http.HttpServletRequest;
     9 import javax.servlet.http.HttpServletResponse;
    10 import java.util.Map;
    11 import java.util.concurrent.ConcurrentHashMap;
    12 
    13 /**
    14  * 请求限流拦截器
    15  *
    16  * @author xhq
    17  * @version 1.0
    18  * @date 2019/10/22 16:46
    19  */
    20 @Component
    21 public class RequestLimiterInterceptor extends GenericInterceptor {
    22 
    23     /**
    24      * 不同的方法存放不同的令牌桶
    25      */
    26     private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
    27 
    28     @Override
    29     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    30         try {
    31             if (handler instanceof HandlerMethod) {
    32                 HandlerMethod handlerMethod = (HandlerMethod) handler;
    33                 RequestLimiter rateLimit = handlerMethod.getMethodAnnotation(RequestLimiter.class);
    34                 //判断是否有注解
    35                 if (rateLimit != null) {
    36                     // 获取请求url
    37                     String url = request.getRequestURI();
    38                     RateLimiter rateLimiter;
    39                     // 判断map集合中是否有创建好的令牌桶
    40                     if (!rateLimiterMap.containsKey(url)) {
    41                         // 创建令牌桶,以n r/s往桶中放入令牌
    42                         rateLimiter = RateLimiter.create(rateLimit.QPS());
    43                         rateLimiterMap.put(url, rateLimiter);
    44                     }
    45                     rateLimiter = rateLimiterMap.get(url);
    46                     // 获取令牌
    47                     boolean acquire = rateLimiter.tryAcquire(rateLimit.timeout(), rateLimit.timeunit());
    48                     if (acquire) {
    49                         //获取令牌成功
    50                         return super.preHandle(request, response, handler);
    51                     } else {
    52                         log.warn("请求被限流,url:{}", request.getServletPath());
    53                         this.write(response, new GenericResult(StateCode.ERROR_SERVER, rateLimit.msg()));
    54                         return false;
    55                     }
    56                 }
    57             }
    58             return true;
    59         } catch (Exception var6) {
    60             var6.printStackTrace();
    61             this.write(response, new GenericResult(StateCode.ERROR, "对不起,请求似乎出现了一些问题,请您稍后重试!"));
    62             return false;
    63         }
    64     }
    65 
    66 }

      4.注册拦截器

     1 /**
     2  * springboot - WebMvcConfig
     3  * 
     4  * @author xhq
     5  * @version 1.0
     6  */
     7 @Configuration
     8 public class WebMvcConfig implements WebMvcConfigurer {
     9 
    10     /**
    11      * 请求限流拦截器
    12      */
    13     @Autowired
    14     protected RequestLimiterInterceptor requestLimiterInterceptor;
    15 
    16     public WebMvcConfig() {}
    17 
    18     @Override
    19     public void addInterceptors(InterceptorRegistry registry) {
    20         // 请求限流
    21         registry.addInterceptor(requestLimiterInterceptor).addPathPatterns("/**");
    22     }
    23 
    24 }

      5.在接口上配置注解

    @RequestLimiter(QPS = 5D, timeout = 200, timeunit = TimeUnit.MILLISECONDS,msg = "服务器繁忙,请稍后再试")
    @GetMapping("/test")
    @ResponseBody
    public String test(){
          return "";
    }

    五.总结

      1.该代码只适于单个应用进行接口限流,如果是分布式项目或者微服务项目可以采用nosql中央缓存(eg:redis)来实现。

      2.除了拦截器,当然也可以用filter和aop来实现。

      

  • 相关阅读:
    用Python发生RestFul API POST和GET请求
    C# 8.0中的新功能
    A股数据分析之收集数据:股票列表和股价
    A股数据分析之收集数据:公司详细信息
    VS 2019中修改C#语言版本
    Weak Event Manager
    在WPF中使用MVVM的方式关闭窗口
    C# GDI绘制仪表盘(纯代码实现)
    C#中实现文件拖放打开的方法
    C#设置自定义文件图标实现双击启动
  • 原文地址:https://www.cnblogs.com/xhq1024/p/11726873.html
Copyright © 2011-2022 走看看