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来实现。

  

posted @ 2019-10-23 16:15  猿了个码  阅读(6400)  评论(0编辑  收藏  举报