面試官:高併發場景,你要如何實現系統限流? 1. 計數器法 2. 漏桶和令牌桶算法 不同限流算法的比較

如果不考慮高併發的情況,即使業務系統平時運行得好好的,併發量一旦增加就會頻繁出現各種詭異的業務問題,比如,在電商業務中,可能會出現用戶訂單丟失、庫存扣減異常、超賣等問題。

限流是服務降級的一種手段,顧名思義,通過限制系統的流量,從而實現保護系統的目的。

合理的限流配置,需要了解系統的吞吐量,所以,限流一般需要結合 容量規劃 和 壓測 來進行。

當外部請求接近或者達到系統的最大閾值時,觸發限流,採取其他的手段進行降級,保護系統不被壓垮。常見的降級策略包括 延遲處理 、 拒絕服務 、 隨機拒絕 等。

限流後的策略,其實和 Java 併發編程中的線程池非常類似,我們都知道,線程池在任務滿的情況下,可以配置不同的拒絕策略,比如:

AbortPolicy,會丟棄任務並拋出異常
DiscardPolicy,丟棄任務,不拋出異常
DiscardOldestPolicy 等,當然也可以自己實現拒絕策略
Java 的線程池是開發中一個小的功能點,但是見微知著,也可以引申到系統的設計和架構上,將知識進行合理地遷移複用。

限流方案中有一點非常關鍵,那就是 如何判斷當前的流量已經達到我們設置的最大值 ,具體有不同的實現策略,下面進行簡單分析。

1. 計數器法

一般來說,我們進行限流時使用的是單位時間內的請求數,也就是平常說的 QPS,統計 QPS 最直接的想法就是實現一個計數器。

計數器法是限流算法裏最簡單的一種算法,我們假設一個接口限制 100 秒內的訪問次數不能超過 10000 次,維護一個計數器,每次有新的請求過來,計數器加 1。

這時候判斷,

如果計數器的值小於限流值,並且與上一次請求的時間間隔還在 100 秒內,允許請求通過,否則拒絕請求
如果超出了時間間隔,要將計數器清零
下面的代碼裏使用 AtomicInteger 作爲計數器,可以作爲參考:

public class CounterLimiter { 
    //初始時間 
    private static long startTime = System.currentTimeMillis(); 
    //初始計數值 
    private static final AtomicInteger ZERO = new AtomicInteger(0); 
    //時間窗口限制 
    private static final int interval = 10000; 
    //限制通過請求 
    private static int limit = 100; 
    //請求計數 
    private AtomicInteger requestCount = ZERO; 
    //獲取限流 
    public boolean tryAcquire() { 
        long now = System.currentTimeMillis(); 
        //在時間窗口內 
        if (now < startTime + interval) { 
            //判斷是否超過最大請求 
            if (requestCount.get() < limit) { 
                requestCount.incrementAndGet(); 
                return true; 
            } 
            return false; 
        } else { 
            //超時重置 
            requestCount = ZERO; 
            startTime = now; 
            return true; 
        } 
    } 
} 

計數器策略進行限流,可以從單點擴展到集羣,適合應用在分佈式環境中。

單點限流使用內存即可,如果擴展到集羣限流,可以用一個單獨的存儲節點,比如 Redis 或者 Memcached 來進行存儲,在固定的時間間隔內設置過期時間,就可以統計集羣流量,進行整體限流。

計數器策略有一個很大的缺點, 對臨界流量不友好,限流不夠平滑 。

假設這樣一個場景,我們限制用戶一分鐘下單不超過 10 萬次,現在在兩個時間窗口的交匯點,前後一秒鐘內,分別發送 10 萬次請求。也就是說,窗口切換的這兩秒鐘內,系統接收了 20 萬下單請求,這個峯值可能會超過系統閾值,影響服務穩定性。

對計數器算法的優化,就是避免出現兩倍窗口限制的請求,可以使用滑動窗口算法實現,感興趣的同學可以去了解一下。

2. 漏桶和令牌桶算法

漏桶算法和令牌桶算法,在實際應用中更加廣泛,也經常被拿來對比。

漏桶算法可以用漏桶來對比,假設現在有一個固定容量的桶,底部鑽一個小孔可以漏水,我們通過控制漏水的速度,來控制請求的處理,實現限流功能。

漏桶算法的拒絕策略很簡單:如果外部請求超出當前閾值,則會在水桶裏積蓄,一直到溢出,系統並不關心溢出的流量。

漏桶算法是從出口處限制請求速率,並不存在上面計數器法的臨界問題,請求曲線始終是平滑的。

它的一個核心問題是 對請求的過濾太精準了 ,我們常說“水至清則無魚”,其實在限流裏也是一樣的,我們限制每秒下單 10 萬次,那 10 萬零 1 次請求呢?是不是必須拒絕掉呢?

大部分業務場景下這個答案是否定的,雖然限流了,但還是希望系統允許一定的突發流量,這時候就需要令牌桶算法。

在令牌桶算法中,假設我們有一個大小恆定的桶,這個桶的容量和設定的閾值有關,桶裏放着很多令牌,通過一個固定的速率,往裏邊放入令牌,如果桶滿了,就把令牌丟掉,最後桶中可以保存的最大令牌數永遠不會超過桶的大小。當有請求進入時,就嘗試從桶裏取走一個令牌,如果桶裏是空的,那麼這個請求就會被拒絕。

不知道你有沒有使用過 Google 的 Guava 開源工具包?在 Guava 中有限流策略的工具類 RateLimiter,RateLimiter 基於令牌桶算法實現流量限制,使用非常方便。

RateLimiter 會按照一定的頻率往桶裏扔令牌,線程拿到令牌才能執行,RateLimter 的 API 可以直接應用,主要方法是 acquire 和 tryAcquire 。

acquire 會阻塞, tryAcquire 方法則是非阻塞的。

下面是一個簡單的示例:

public class LimiterTest { 
    public static void main(String[] args) throws InterruptedException { 
        //允許10個,permitsPerSecond 
        RateLimiter limiter = RateLimiter.create(100); 
        for(int i=1;i<200;i++){ 
            if (limiter.tryAcquire(1)){ 
                System.out.println("第"+i+"次請求成功"); 
            }else{ 
                System.out.println("第"+i+"次請求拒絕"); 
            } 
        } 
    } 
} 

不同限流算法的比較

計數器算法實現比較簡單,特別適合集羣情況下使用,但是要考慮臨界情況,可以應用滑動窗口策略進行優化,當然也是要看具體的限流場景。

漏桶算法和令牌桶算法,漏桶算法提供了比較嚴格的限流,令牌桶算法在限流之外,允許一定程度的突發流量。在實際開發中,我們並不需要這麼精準地對流量進行控制,所以令牌桶算法的應用更多一些。

如果我們設置的流量峯值是 permitsPerSecond=N ,也就是每秒鐘的請求量,計數器算法會出現 2N 的流量,漏桶算法會始終限制 N 的流量,而令牌桶算法允許大於 N,但不會達到 2N 這麼高的峯值。

來源:https://www.tuicool.com/articles/fA3Enym

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章