高併發系統限流中的演算法

dongyu7136發表於2018-05-16

來自 https://blog.csdn.net/scorpio3k/article/details/53103239
在大資料量高併發訪問時,經常會出現服務或介面面對暴漲的請求而不可用的情況,甚至引發連鎖反映導致整個系統崩潰。此時你需要使用的技術手段之一就是限流,當請求達到一定的併發數或速率,就進行等待、排隊、降級、拒絕服務等。在限流時,常見的三種演算法是漏桶、令牌桶演算法演算法 、計數器限流演算法,本文即對相關內容進行重點介紹。
一、漏桶演算法的概念
漏桶演算法(Leaky Bucket):主要目的是控制資料注入到網路的速率,平滑網路上的突發流量。漏桶演算法提供了一種機制,通過它,突發流量可以被整形以便為網路提供一個穩定的流量。漏桶演算法的示意圖如下:
請求先進入到漏桶裡,漏桶以一定的速度出水,當水請求過大會直接溢位,可以看出漏桶演算法能強行限制資料的傳輸速率。
二、令牌桶演算法的概念
令牌桶演算法(Token Bucket):是網路流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種演算法。典型情況下,令牌桶演算法用來控制傳送到網路上的資料的數目,並允許突發資料的傳送。令牌桶演算法示意圖如下所示:
大小固定的令牌桶可自行以恆定的速率源源不斷地產生令牌。如果令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢位。最後桶中可以儲存的最大令牌數永遠不會超過桶的大小。
Guava是google提供的java擴充套件類庫,其中的限流工具類RateLimiter採用的就是令牌桶演算法。RateLimiter 從概念上來講,速率限制器會在可配置的速率下分配許可證,如果必要的話,每個acquire() 會阻塞當前執行緒直到許可證可用後獲取該許可證,一旦獲取到許可證,不需要再釋放許可證。通俗的講RateLimiter會按照一定的頻率往桶裡扔令牌,執行緒拿到令牌才能執行,比如你希望自己的應用程式QPS不要超過1000,那麼RateLimiter設定1000的速率後,就會每秒往桶裡扔1000個令牌。例如我們需要處理一個任務列表,但我們不希望每秒的任務提交超過兩個,此時可以採用如下方式:

public class RateLimiterDemo {
    private static RateLimiter limiter = RateLimiter.create(5);

    public static void exec() {
        limiter.acquire(1);
        try {
            // 處理核心邏輯
            TimeUnit.SECONDS.sleep(1);
            System.out.println("--" + System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

有一點很重要,那就是請求的許可數從來不會影響到請求本身的限制(呼叫acquire(1) 和呼叫acquire(1000) 將得到相同的限制效果,如果存在這樣的呼叫的話),但會影響下一次請求的限制,也就是說,如果一個高開銷的任務抵達一個空閒的RateLimiter,它會被馬上許可,但是下一個請求會經歷額外的限制,從而來償付高開銷任務。注意:RateLimiter 並不提供公平性的保證。
來自https://www.cnblogs.com/java1024/p/7725632.html
三、計數器限流演算法的概念
計數器限流演算法也是比較常用的,主要用來限制總併發數,比如資料庫連線池大小、執行緒池大小、程式訪問併發數等都是使用計數器演算法。
使用計數器限流示例1

public class CountRateLimiterDemo1 {

    private static AtomicInteger count = new AtomicInteger(0);

    public static void exec() {
        if (count.get() >= 5) {
            System.out.println("請求使用者過多,請稍後在試!"+System.currentTimeMillis()/1000);
        } else {
            count.incrementAndGet();
            try {
                //處理核心邏輯
                TimeUnit.SECONDS.sleep(1);
                System.out.println("--"+System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                count.decrementAndGet();
            }
        }
    }
}

使用AomicInteger來進行統計當前正在併發執行的次數,如果超過域值就簡單粗暴的直接響應給使用者,說明系統繁忙,請稍後再試或其它跟業務相關的資訊。
弊端:使用 AomicInteger 簡單粗暴超過域值就拒絕請求,可能只是瞬時的請求量高,也會拒絕請求。
使用計數器限流示例2

public class CountRateLimiterDemo2 {

    private static Semaphore semphore = new Semaphore(5);

    public static void exec() {
        if(semphore.getQueueLength()>100){
            System.out.println("當前等待排隊的任務數大於100,請稍候再試...");
        }
        try {
            semphore.acquire();
            // 處理核心邏輯
            TimeUnit.SECONDS.sleep(1);
            System.out.println("--" + System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semphore.release();
        }
    }

使用Semaphore訊號量來控制併發執行的次數,如果超過域值訊號量,則進入阻塞佇列中排隊等待獲取訊號量進行執行。如果阻塞佇列中排隊的請求過多超出系統處理能力,則可以在拒絕請求。

相對Atomic優點:如果是瞬時的高併發,可以使請求在阻塞佇列中排隊,而不是馬上拒絕請求,從而達到一個流量削峰的目的。

相關文章