高效能限流器Guava RateLimiter
首先我們來看看 Guava RateLimiter 是如何解決高併發場景下的限流問題的。Guava 是 Google 開源的 Java 類庫,提供了一個工具類 RateLimiter。我們先來看看 RateLimiter 的使用,讓你對限流有個感官的印象。假設我們有一個執行緒池,它每秒只能處理兩個任務,如果提交的任務過快,可能導致系統不穩定,這個時候就需要用到限流。
在下面的示例程式碼中,我們建立了一個流速為 2 個請求 / 秒的限流器,這裡的流速該怎麼理解呢?直觀地看,2 個請求 / 秒指的是每秒最多允許 2 個請求通過限流器,其實在 Guava 中,流速還有更深一層的意思:是一種勻速的概念,2 個請求 / 秒等價於 1 個請求 /500 毫秒。
在向執行緒池提交任務之前,呼叫 acquire() 方法就能起到限流的作用。通過示例程式碼的執行結果,任務提交到執行緒池的時間間隔基本上穩定在 500 毫秒。
//限流器流速:2個請求/秒 RateLimiter limiter = RateLimiter.create(2.0); //執行任務的執行緒池 ExecutorService es = Executors .newFixedThreadPool(1); //記錄上一次執行時間 prev = System.nanoTime(); //測試執行20次 for (int i=0; i<20; i++){ //限流器限流 limiter.acquire(); //提交任務非同步執行 es.execute(()->{ long cur=System.nanoTime(); //列印時間間隔:毫秒 System.out.println( (cur-prev)/1000_000); prev = cur; }); } 輸出結果: ... 500 499 499 500 499
經典限流演算法:令牌桶演算法
Guava 的限流器使用上還是很簡單的,那它是如何實現的呢?Guava 採用的是令牌桶演算法,其核心是要想通過限流器,必須拿到令牌。也就是說,只要我們能夠限制發放令牌的速率,那麼就能控制流速了。令牌桶演算法的詳細描述如下:
-
令牌以固定的速率新增到令牌桶中,假設限流的速率是 r/ 秒,則令牌每 1/r 秒會新增一個;
-
假設令牌桶的容量是 b ,如果令牌桶已滿,則新的令牌會被丟棄;
-
請求能夠通過限流器的前提是令牌桶中有令牌。
這個演算法中,限流的速率 r 還是比較容易理解的,但令牌桶的容量 b 該怎麼理解呢?b 其實是 burst 的簡寫,意義是限流器允許的最大突發流量
比如 b=10,而且令牌桶中的令牌已滿,此時限流器允許 10 個請求同時通過限流器,當然只是突發流量而已,這 10 個請求會帶走 10 個令牌,所以後續的流量只能按照速率 r 通過限流器。
令牌桶這個演算法,如何用 Java 實現呢?很可能你的直覺會告訴你生產者 - 消費者模式:一個生產者執行緒定時向阻塞佇列中新增令牌,而試圖通過限流器的執行緒則作為消費者執行緒,只有從阻塞佇列中獲取到令牌,才允許通過限流器。
這個演算法看上去非常完美,而且實現起來非常簡單,如果併發量不大,這個實現並沒有什麼問題。可實際情況卻是使用限流的場景大部分都是高併發場景,而且系統壓力已經臨近極限了,此時這個實現就有問題了。問題就出在定時器上,在高併發場景下,當系統壓力已經臨近極限的時候,定時器的精度誤差會非常大,同時定時器本身會建立排程執行緒,也會對系統的效能產生影響。
那還有什麼好的實現方式呢?當然有,Guava 的實現就沒有使用定時器,下面我們就來看看它是如何實現的。
Guava 如何實現令牌桶演算法
Guava 實現令牌桶演算法,用了一個很簡單的辦法,其關鍵是記錄並動態計算下一令牌發放的時間。下面我們以一個最簡單的場景來介紹該演算法的執行過程。假設令牌桶的容量為 b=1,限流速率 r = 1 個請求 / 秒,如下圖所示,如果當前令牌桶中沒有令牌,下一個令牌的發放時間是在第 3 秒,而在第 2 秒的時候有一個執行緒 T1 請求令牌,此時該如何處理呢?
總結
經典的限流演算法有兩個,一個是令牌桶演算法(Token Bucket),另一個是漏桶演算法(Leaky Bucket)。令牌桶演算法是定時向令牌桶傳送令牌,請求能夠從令牌桶中拿到令牌,然後才能通過限流器;而漏桶演算法裡,請求就像水一樣注入漏桶,漏桶會按照一定的速率自動將水漏掉,只有漏桶裡還能注入水的時候,請求才能通過限流器。令牌桶演算法和漏桶演算法很像一個硬幣的正反面,所以你可以參考令牌桶演算法的實現來實現漏桶演算法。
上面我們介紹了 Guava 是如何實現令牌桶演算法的,我們的示例程式碼是對 Guava RateLimiter 的簡化,Guava RateLimiter 擴充套件了標準的令牌桶演算法,比如還能支援預熱功能。對於按需載入的快取來說,預熱後快取能支援 5 萬 TPS 的併發,但是在預熱前 5 萬 TPS 的併發直接就把快取擊垮了,所以如果需要給該快取限流,限流器也需要支援預熱功能,在初始階段,限制的流速 r 很小,但是動態增長的。預熱功能的實現非常複雜,Guava 構建了一個積分函式來解決這個問題,如果你感興趣,可以繼續深入研究。