引言
在開發高並發系統時有三把利器用來保護系統:緩存、降級和限流。今天我們要聊的就是限流(Rate Limit),限流的目的很簡單,就是為了保護系統不被瞬時大流量沖垮,
限流這個概念我其實很早之前就有去了解過,不過無奈之前工作所接觸業務的並發量實在是談不上限流。目前公司大促峰值QPS在2w往上,自然而然需要用到限流,特別是類似秒殺這種瞬時流量非常大但實際成單率低的業務場景。
目前比較常用的限流算法有三種
-
計數器固定窗口算法
-
計數器滑動窗口算法
-
漏桶算法
-
令牌桶算法
計數器固定窗口算法
計數器固定窗口算法是最簡單的限流算法,實現方式也比較簡單。就是通過維護一個單位時間內的計數值,每當一個請求通過時,就將計數值加1,當計數值超過預先設定的閾值時,就拒絕單位時間內的其他請求。如果單位時間已經結束,則將計數器清零,開啟下一輪的計數。
但是這種實現會有一個問題,舉個例子:
假設我們設定1秒內允許通過的請求閾值是200,如果有用戶在時間窗口的最后幾毫秒發送了200個請求,緊接着又在下一個時間窗口開始時發送了200個請求,那么這個用戶其實在一秒內成功請求了400次,顯然超過了閾值但並不會被限流。其實這就是臨界值問題,那么臨界值問題要怎么解決呢?
- 代碼實現 -- [CounterRateLimit.java](https://github.com/WangJunnan/learn/blob/master/algorithm/src/main/java/com/walm/learn/algorithm/ratelimit/CounterRateLimit.java)
計數器滑動窗口算法
計數器滑動窗口法就是為了解決上述固定窗口計數存在的問題而誕生,學過TCP協議的同學應該對滑動窗口不陌生,其實還是不太一樣的,下文我們要說的滑動窗口是基於時間來划分窗口的。而TCP的滑動窗口指的是能夠接受的字節數,並且大小是可變的(擁塞控制)
滑動窗口是怎么做的?
前面說了固定窗口存在臨界值問題,要解決這種臨界值問題,顯然只用一個窗口是解決不了問題的。假設我們仍然設定1秒內允許通過的請求是200個,但是在這里我們需要把1秒的時間分成多格,假設分成5格(格數越多,流量過渡越平滑),每格窗口的時間大小是200毫秒,每過200毫秒,就將窗口向前移動一格。為了便於理解,可以看下圖

圖中將窗口划為5份,每個小窗口中的數字表示在這個窗口中請求數,所以通過觀察上圖,可知在當前時間快(200毫秒)允許通過的請求數應該是20而不是200(只要超過20就會被限流),因為我們最終統計請求數時是需要把當前窗口的值進行累加,進而得到當前請求數來判斷是不是需要進行限流。
那么滑動窗口限流法是完美的嗎?
細心觀察的我們應該能馬上發現問題,滑動窗口限流法其實就是計數器固定窗口算法的一個變種。流量的過渡是否平滑依賴於我們設置的窗口格數也就是統計時間間隔,格數越多,統計越精確,但是具體要分多少格我們也說不上來呀...
- 代碼實現 -- [SlidingWindowRateLimit.java](https://github.com/WangJunnan/learn/blob/master/algorithm/src/main/java/com/walm/learn/algorithm/ratelimit/SlidingWindowRateLimit.java)
漏桶算法
上面所介紹的兩種算法都不能非常平滑的過渡,下面就是漏桶算法登場了
什么是漏桶算法?
漏桶算法以一個常量限制了出口流量速率,因此漏桶算法可以平滑突發的流量。其中漏桶作為流量容器我們可以看做一個FIFO的隊列,當入口流量速率大於出口流量速率時,因為流量容器是有限的,當超出流量容器大小時,超出的流量會被丟棄。
下圖比較形象的說明了漏桶算法的原理,其中水龍頭是入口流量,漏桶是流量容器,勻速流出的水是出口流量。

漏桶算法的特點
-
漏桶具有固定容量,出口流量速率是固定常量(流出請求)
-
入口流量可以以任意速率流入到漏桶中(流入請求)
-
如果入口流量超出了桶的容量,則流入流量會溢出(新請求被拒絕)
-
代碼實現 -- [LeakyBucketRateLimit.java](https://github.com/WangJunnan/learn/blob/master/algorithm/src/main/java/com/walm/learn/algorithm/ratelimit/LeakyBucketRateLimit.java)
不過因為漏桶算法限制了流出速率是一個固定常量值,所以漏桶算法不支持出現突發流出流量。但是在實際情況下,流量往往是突發的。
令牌桶算法
令牌桶算法是漏桶算法的改進版,可以支持突發流量。不過與漏桶算法不同的是,令牌桶算法的漏桶中存放的是令牌而不是流量。
那么令牌桶算法是怎么突發流量的呢?
最開始,令牌桶是空的,我們以恆定速率往令牌桶里加入令牌,令牌桶被裝滿時,多余的令牌會被丟棄。當請求到來時,會先嘗試從令牌桶獲取令牌(相當於從令牌桶移除一個令牌),獲取成功則請求被放行,獲取失敗則阻塞活拒絕請求。

令牌桶算法的特點
-
最多可以存發b個令牌。如果令牌到達時令牌桶已經滿了,那么這個令牌會被丟棄
-
請求到來時,如果令牌桶中少於n個令牌,那么不會刪除令牌。該請求會被限流(阻塞活拒絕)
-
算法允許最大b(令牌桶大小)個請求的突發
令牌桶算法限制的是平均流量,因此其允許突發流量(只要令牌桶中有令牌,就不會被限流)
- 代碼實現 -- [TokenBucketRateLimit.java](https://github.com/WangJunnan/learn/blob/master/algorithm/src/main/java/com/walm/learn/algorithm/ratelimit/TokenBucketRateLimit.java)
總結
至此,基本把以上4種限流算法的原理都解釋清楚了。每種限流算法都有其固定特點,及各自適用的場景,其中計數器算法是其中最簡單的,相當於滑動窗口算法的簡化版,令牌桶算法相比漏桶算法對資源的利用率更高(允許突發流量)
