一.前言
對於一個應用系統來說,我們有時會遇到極限並發的情況,即有一個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來實現。
