高併發場景,你要如何實現系統限流?

本文來源於公衆號:勾勾的Java宇宙(微信號:Javagogo)

原文連接: mp.weixin.qq.com/s/z6LNroSuY…

做者:邴越

對於業務系統來講高併發就是支撐「海量用戶請求」,QPS 會是平時的幾百倍甚至更高。算法

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

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

合理的限流配置,須要瞭解系統的吞吐量,因此,限流通常須要結合容量規劃壓測來進行。markdown

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

限流後的策略,其實和 Java 併發編程中的線程池很是相似,咱們都知道,線程池在任務滿的狀況下,能夠配置不一樣的拒絕策略,好比:併發

  • AbortPolicy,會丟棄任務並拋出異常app

  • DiscardPolicy,丟棄任務,不拋出異常分佈式

  • DiscardOldestPolicy 等,固然也能夠本身實現拒絕策略ide

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 能夠直接應用,主要方法是 acquiretryAcquire

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 這麼高的峯值。


歡迎大佬們關注公衆號 勾勾的Java宇宙(微信號:Javagogo),拒絕水文,收穫乾貨!

相關文章
相關標籤/搜索