面試必備:四種經典限流演算法講解

帶你聊技術發表於2024-03-01

前言

大家好,我是田螺.

最近一位朋友去拼夕夕面試,被問了這麼一道題:限流演算法有哪些?用程式碼實現令牌桶演算法。跟星球好友討論了一波,發現大家都忘記得差不多了.所以田螺哥再整理一波,常見的四種限流演算法,以及簡單程式碼實現,相信大家看完,會茅塞頓開的。

面試必備:四種經典限流演算法講解

1. 固定視窗限流演算法

1.1 什麼是固定視窗限流演算法

固定視窗限流演算法(Fixed Window Rate Limiting Algorithm)是一種最簡單的限流演算法,其原理是在固定時間視窗(單位時間)內限制請求的數量。該演算法將時間分成固定的視窗,並在每個視窗內限制請求的數量。具體來說,演算法將請求按照時間順序放入時間視窗中,並計算該時間視窗內的請求數量,如果請求數量超出了限制,則拒絕該請求。

假設單位時間(固定時間視窗)是1秒,限流閥值為3。在單位時間1秒內,每來一個請求,計數器就加1,如果計數器累加的次數超過限流閥值3,後續的請求全部拒絕。等到1s結束後,計數器清0,重新開始計數。如下圖:

面試必備:四種經典限流演算法講解

1.2 固定視窗限流的虛擬碼

   public static Integer counter = 0;  //統計請求數
   public static long lastAcquireTime =  0L;
   public static final Long windowUnit = 1000L ; //假設固定時間視窗是1000ms
   public static final Integer threshold = 10; // 視窗閥值是10
   
    /**
     * 固定視窗時間演算法
     * 關注公眾號:撿田螺的小男孩
     * @return
     */
    public synchronized boolean fixedWindowsTryAcquire() {
        long currentTime = System.currentTimeMillis();  //獲取系統當前時間
        if (currentTime - lastAcquireTime > windowUnit) {  //檢查是否在時間視窗內
            counter = 0;  // 計數器清0
            lastAcquireTime = currentTime;  //開啟新的時間視窗
        }
        if (counter < threshold) {  // 小於閥值
            counter++;  //計數統計器加1
            return true;
        }

        return false;
    }

1.2 固定視窗演算法的優缺點

  • 優點:固定視窗演算法非常簡單,易於實現和理解。
  • 缺點:存在明顯的臨界問題,比如: 假設限流閥值為5個請求,單位時間視窗是1s,如果我們在單位時間內的前0.8-1s1-1.2s,分別併發5個請求。雖然都沒有超過閥值,但是如果算0.8-1.2s,則併發數高達10,已經超過單位時間1s不超過5閥值的定義啦。
面試必備:四種經典限流演算法講解

2. 滑動視窗限流演算法

2.1 什麼是滑動視窗限流演算法

滑動視窗限流演算法是一種常用的限流演算法,用於控制系統對外提供服務的速率,防止系統被過多的請求壓垮。它將單位時間週期分為n個小週期,分別記錄每個小週期內介面的訪問次數,並且根據時間滑動刪除過期的小週期。它可以解決固定視窗臨界值的問題

用一張圖解釋滑動視窗演算法,如下:

面試必備:四種經典限流演算法講解

假設單位時間還是1s,滑動視窗演算法把它劃分為5個小週期,也就是滑動視窗(單位時間)被劃分為5個小格子。每格表示0.2s。每過0.2s,時間視窗就會往右滑動一格。然後呢,每個小週期,都有自己獨立的計數器,如果請求是0.83s到達的,0.8~1.0s對應的計數器就會加1

我們來看下,滑動視窗,去解決固定視窗限流演算法的臨界問題,思想是怎樣

假設我們1s內的限流閥值還是5個請求,0.8~1.0s內(比如0.9s的時候)來了5個請求,落在黃色格子裡。時間過了1.0s這個點之後,又來5個請求,落在紫色格子裡。如果是固定視窗演算法,是不會被限流的,但是滑動視窗的話,每過一個小週期,它會右移一個小格。過了1.0s這個點後,會右移一小格,當前的單位時間段是0.2~1.2s,這個區域的請求已經超過限定的5了,已觸發限流啦,實際上,紫色格子的請求都被拒絕啦。

當滑動視窗的格子週期劃分的越多,那麼滑動視窗的滾動就越平滑,限流的統計就會越精確

2.2 滑動視窗限流演算法的虛擬碼實現

 /**
     * 單位時間劃分的小週期(單位時間是1分鐘,10s一個小格子視窗,一共6個格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分鐘限流請求數
     */
    private int thresholdPerMin = 100;

    /**
     * 計數器, k-為當前視窗的開始時間值秒,value為當前視窗的計數
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑動視窗時間演算法實現
     */
     public synchronized boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //獲取當前時間在哪個小週期視窗
        int currentWindowNum = countCurrentWindow(currentWindowTime); //當前視窗總請求數

        //超過閥值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //計數器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 統計當前視窗的請求數
    */
    private int countCurrentWindow(long currentWindowTime) {
        //計算視窗開始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍歷儲存的計數器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 刪除無效過期的子視窗計數器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加當前視窗的所有計數器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

2.3 滑動視窗限流演算法的優缺點

優點

  • 簡單易懂
  • 精度高(透過調整時間視窗的大小來實現不同的限流效果)
  • 可擴充套件性強(可以非常容易地與其他限流演算法結合使用)

缺點

  • 突發流量無法處理(無法應對短時間內的大量請求,但是一旦到達限流後,請求都會直接暴力被拒絕。醬紫我們會損失一部分請求,這其實對於產品來說,並不太友好),需要合理調整時間視窗大小。

3. 漏桶限流演算法

3.1 什麼是漏桶限流演算法

漏桶限流演算法(Leaky Bucket Algorithm)是一種流量控制演算法,用於控制流入網路的資料速率,以防止網路擁塞。它的思想是將資料包看作是水滴,漏桶看作是一個固定容量的水桶,資料包像水滴一樣從桶的頂部流入桶中,並透過桶底的一個小孔以一定的速度流出,從而限制了資料包的流量。

漏桶限流演算法的基本工作原理是:對於每個到來的資料包,都將其加入到漏桶中,並檢查漏桶中當前的水量是否超過了漏桶的容量。如果超過了容量,就將多餘的資料包丟棄。如果漏桶中還有水,就以一定的速率從桶底輸出資料包,保證輸出的速率不超過預設的速率,從而達到限流的目的。

面試必備:四種經典限流演算法講解
  • 流入的水滴,可以看作是訪問系統的請求,這個流入速率是不確定的。
  • 桶的容量一般表示系統所能處理的請求數。
  • 如果桶的容量滿了,就達到限流的閥值,就會丟棄水滴(拒絕請求)
  • 流出的水滴,是恆定過濾的,對應服務按照固定的速率處理請求。

3.2 漏桶限流演算法的虛擬碼實現

 /**
 * LeakyBucket 類表示一個漏桶,
 * 包含了桶的容量和漏桶出水速率等引數,
 * 以及當前桶中的水量和上次漏水時間戳等狀態。
 */
public class LeakyBucket {
    private final long capacity;    // 桶的容量
    private final long rate;        // 漏桶出水速率
    private long water;             // 當前桶中的水量
    private long lastLeakTimestamp; // 上次漏水時間戳

    public LeakyBucket(long capacity, long rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastLeakTimestamp = System.currentTimeMillis();
    }

    /**
     * tryConsume() 方法用於嘗試向桶中放入一定量的水,如果桶中還有足夠的空間,則返回 true,否則返回 false
     * @param waterRequested
     * @return
     */
    public synchronized boolean tryConsume(long waterRequested) {
        leak();
        if (water + waterRequested <= capacity) {
            water += waterRequested;
            return true;
        } else {
            return false;
        }
    }

    /**
     * 。leak() 方法用於漏水,根據當前時間和上次漏水時間戳計算出應該漏出的水量,然後更新桶中的水量和漏水時間戳等狀態。
     */
    private void leak() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastLeakTimestamp;
        long leakedWater = elapsedTime * rate / 1000;
        if (leakedWater > 0) {
            water = Math.max(0, water - leakedWater);
            lastLeakTimestamp = now;
        }
    }
}

  • 注意:   tryConsume() leak() 方法中,都需要對桶的狀態進行同步,以保證執行緒安全性。

3.3 漏桶限流演算法的優缺點

優點

  • 可以平滑限制請求的處理速度,避免瞬間請求過多導致系統崩潰或者雪崩。
  • 可以控制請求的處理速度,使得系統可以適應不同的流量需求,避免過載或者過度閒置。
  • 可以透過調整桶的大小和漏出速率來滿足不同的限流需求,可以靈活地適應不同的場景。

缺點

  • 需要對請求進行快取,會增加伺服器的記憶體消耗。
  • 對於流量波動比較大的場景,需要較為靈活的引數配置才能達到較好的效果。
  • 但是面對突發流量的時候,漏桶演算法還是循規蹈矩地處理請求,這不是我們想看到的啦。流量變突發時,我們肯定希望系統儘量快點處理請求,提升使用者體驗嘛。

4. 令牌桶演算法

4.1 什麼是令牌桶演算法

令牌桶演算法是一種常用的限流演算法,可以用於限制單位時間內請求的數量。該演算法維護一個固定容量的令牌桶,每秒鐘會向令牌桶中放入一定數量的令牌。當有請求到來時,如果令牌桶中有足夠的令牌,則請求被允許透過並從令牌桶中消耗一個令牌,否則請求被拒絕。

面試必備:四種經典限流演算法講解

4.2 令牌桶演算法的虛擬碼實現

/**
 * TokenBucket 類表示一個令牌桶
 */
public class TokenBucket {

    private final int capacity;     // 令牌桶容量
    private final int rate;         // 令牌生成速率,單位:令牌/秒
    private int tokens;             // 當前令牌數量
    private long lastRefillTimestamp;  // 上次令牌生成時間戳

    /**
     * 建構函式中傳入令牌桶的容量和令牌生成速率。
     * @param capacity
     * @param rate
     */
    public TokenBucket(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = capacity;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    /**
     * allowRequest() 方法表示一個請求是否允許透過,該方法使用 synchronized 關鍵字進行同步,以保證執行緒安全。
     * @return
     */
    public synchronized boolean allowRequest() {
        refill();
        if (tokens > 0) {
            tokens--;
            return true;
        } else {
            return false;
        }
    }

    /**
     * refill() 方法用於生成令牌,其中計算令牌數量的邏輯是按照令牌生成速率每秒鐘生成一定數量的令牌,
     * tokens 變數表示當前令牌數量,
     * lastRefillTimestamp 變數表示上次令牌生成的時間戳。
     */
    private void refill() {
        long now = System.currentTimeMillis();
        if (now > lastRefillTimestamp) {
            int generatedTokens = (int) ((now - lastRefillTimestamp) / 1000 * rate);
            tokens = Math.min(tokens + generatedTokens, capacity);
            lastRefillTimestamp = now;
        }
    }
}

4.3 令牌桶演算法的優缺點

優點:

  • 穩定性高:令牌桶演算法可以控制請求的處理速度,可以使系統的負載變得穩定。
  • 精度高:令牌桶演算法可以根據實際情況動態調整生成令牌的速率,可以實現較高精度的限流。
  • 彈性好:令牌桶演算法可以處理突發流量,可以在短時間內提供更多的處理能力,以處理突發流量。

GuavaRateLimiter限流元件,就是基於令牌桶演算法實現的。

缺點:

  • 實現複雜:相對於固定視窗演算法等其他限流演算法,令牌桶演算法的實現較為複雜。對短時請求難以處理:在短時間內有大量請求到來時,可能會導致令牌桶中的令牌被快速消耗完,從而限流。這種情況下,可以考慮使用漏桶演算法。
  • 時間精度要求高:令牌桶演算法需要在固定的時間間隔內生成令牌,因此要求時間精度較高,如果系統時間不準確,可能會導致限流效果不理想。

總體來說,令牌桶演算法具有較高的穩定性和精度,但實現相對複雜,適用於對穩定性和精度要求較高的場景。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2939334/,如需轉載,請註明出處,否則將追究法律責任。

相關文章