高併發優雅的作限流

image

技術分析

若是你比較關注如今的技術形式,就會知道微服務如今火的一塌糊塗,固然,事物都有兩面性,微服務也不是解決技術,架構等問題的萬能鑰匙。若是服務化帶來的利大於弊,菜菜仍是推薦將系統服務化。隨着服務化的進程的不斷演化,各類概念以及技術隨之而來。任何一種方案都是爲了解決問題而存在。好比:熔斷設計,接口冪等性設計,重試機制設計,還有今天菜菜要說的限流設計,等等這些技術幾乎都充斥在每一個系統中。php

就今天來講的限流,書面意思和做用一致,就是爲了限制,經過對併發訪問或者請求進行限速或者一個時間窗口內的請求進行限速來保護系統。一旦達到了限制的臨界點,能夠用拒絕服務、排隊、或者等待的方式來保護現有系統,不至於發生雪崩現象。java

限流就像作帝都的地鐵通常,若是你住在西二旗或者天通苑也許會體會的更深入一些。我更習慣在技術角度用消費者的角度來闡述,須要限流的通常緣由是消費者能力有限,目的爲了不超過消費者能力而出現系統故障。固然也有其餘相似的狀況也能夠用限流來解決。golang

限流的表現形式上大部分能夠分爲兩大類:算法

  1. 限制消費者數量。也能夠說消費的最大能力值。好比:數據庫的鏈接池是側重的是總的鏈接數。還有菜菜之前寫的線程池,本質上也是限制了消費者的最大消費能力。
  2. 能夠被消費的請求數量。這裏的數量能夠是瞬時併發數,也能夠是一段時間內的總併發數。菜菜今天要幫YY妹子作的也是這個。

除此以外,限流還有別的表現形式,例如按照網絡流量來限流,按照cpu使用率來限流等。按照限流的範圍又能夠分爲分佈式限流,應用限流,接口限流等。不管怎麼變化,限流均可以用如下圖來表示:數據庫

image

經常使用技術實現

令牌桶算法

令牌桶是一個存放固定容量令牌的桶,按照固定速率往桶裏添加令牌,填滿了就丟棄令牌,請求是否被處理要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求。令牌桶容許必定程度突發流量,只要有令牌就能夠處理,支持一次拿多個令牌。令牌桶中裝的是令牌。
image設計模式

漏桶算法

漏桶一個固定容量的漏桶,按照固定常量速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕。漏桶能夠看作是一個具備固定容量、固定流出速率的隊列,漏桶限制的是請求的流出速率。漏桶中裝的是請求。
imageapi

計數器

有時咱們還會使用計數器來進行限流,主要用來限制必定時間內的總併發數,好比數據庫鏈接池、線程池、秒殺的併發數;計數器限流只要必定時間內的總請求數超過設定的閥值則進行限流,是一種簡單粗暴的總數量限流,而不是平均速率限流。數組

除此以外,其實根據不一樣的業務場景,還能夠出現不少不一樣的限流算法,可是總的規則只有一條:只要符合當前業務場景的限流策略就是最好的

限流的其餘基礎知識請百度!!服務器

另外一種方式解決妹子問題

迴歸問題,YY妹子的問題,菜菜不許備用以上所說的幾種算法來幫助她。菜菜準備用一個按照時間段限制請求總數的方式來限流。 整體思路是這樣:網絡

  1. 用一個環形來表明經過的請求容器。
  2. 用一個指針指向當前請求所到的位置索引,來判斷當前請求時間和當前位置上次請求的時間差,依此來判斷是否被限制。
  3. 若是請求經過,則當前指針向前移動一個位置,不經過則不移動位置
  4. 重複以上步驟 直到永遠.......

image

用代碼說話纔是王道

如下代碼不改或者稍微修改可用於生產環境

如下代碼的核心思路是這樣的:指針當前位置的時間元素和當前時間的差來決定是否容許這次請求,這樣經過的請求在時間上表現的比較平滑。

思路遠比語言重要,任何語言也可爲之,請phper,golanger,javaer 自行實現一遍便可
//限流組件,採用數組作爲一個環
    class LimitService
    {
        //當前指針的位置
        int currentIndex = 0;
        //限制的時間的秒數,即:x秒容許多少請求
        int limitTimeSencond = 1;
        //請求環的容器數組
        DateTime?[] requestRing = null;
        //容器改變或者移動指針時候的鎖
        object objLock = new object();

        public LimitService(int countPerSecond,int  _limitTimeSencond)
        {
            requestRing = new DateTime?[countPerSecond];
            limitTimeSencond= _limitTimeSencond;
        }

        //程序是否能夠繼續
        public bool IsContinue()
        {
            lock (objLock)
            {
                var currentNode = requestRing[currentIndex];
                //若是當前節點的值加上設置的秒 超過當前時間,說明超過限制
                if (currentNode != null&& currentNode.Value.AddSeconds(limitTimeSencond) >DateTime.Now)
                {
                    return false;
                }
                //當前節點設置爲當前時間
                requestRing[currentIndex] = DateTime.Now;
                //指針移動一個位置
                MoveNextIndex(ref currentIndex);
            }            
            return true;
        }
        //改變每秒能夠經過的請求數
        public bool ChangeCountPerSecond(int countPerSecond)
        {
            lock (objLock)
            {
                requestRing = new DateTime?[countPerSecond];
                currentIndex = 0;
            }
            return true;
        }

        //指針往前移動一個位置
        private void MoveNextIndex(ref int currentIndex)
        {
            if (currentIndex != requestRing.Length - 1)
            {
                currentIndex = currentIndex + 1;
            }
            else
            {
                currentIndex = 0;
            }
        }
    }

測試程序以下:

static  LimitService l = new LimitService(1000, 1);
        static void Main(string[] args)
        {
            int threadCount = 50;
            while (threadCount >= 0)
            {
                Thread t = new Thread(s =>
                {
                    Limit();
                });
                t.Start();
                threadCount--;
            }           

            Console.Read();
        }

        static void Limit()
        {
            int i = 0;
            int okCount = 0;
            int noCount = 0;
            Stopwatch w = new Stopwatch();
            w.Start();
            while (i < 1000000)
            {
                var ret = l.IsContinue();
                if (ret)
                {
                    okCount++;
                }
                else
                {
                    noCount++;
                }
                i++;
            }
            w.Stop();
            Console.WriteLine($"共用{w.ElapsedMilliseconds},容許:{okCount},  攔截:{noCount}");
        }

測試結果以下:

image

image

最大用時15秒,共處理請求1000000*50=50000000 次
並未發生GC操做,內存使用率很是低,每秒處理 300萬次+請求 。以上程序修改成10個線程,大約用時4秒以內
image

若是是強勁的服務器或者線程數較少狀況下處理速度將會更快

寫在最後

以上代碼雖然簡單,可是卻爲限流的核心代碼(其實還有優化餘地),通過其餘封裝能夠適用於Webapi的filter或其餘場景。妹子問題解決了,要不要讓她請我吃個飯呢?

更多精彩文章

image

相關文章
相關標籤/搜索