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來實現。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM