Redis實現限流的幾種方式

參考文章:https://zhuanlan.zhihu.com/p/439093222

https://mp.weixin.qq.com/s/zf9uqfJfRYvmSVXUQofF2A

 

互聯網應用往往是高併發的場景,互聯網的特性就是瞬時、激增,比如鹿晗官宣了,此時,如果沒有流量管控,很容易導致系統雪崩。

而限流是用來保證系統穩定性的常用手段,當系統遭遇瞬時流量激增,很可能會因系統資源耗盡導致宕機,限流可以把一超出系統承受能力外的流量直接拒絕掉,保證大部分流量可以正常訪問,從而保證系統只接收承受範圍以內的請求。

我們常用的限流算法有:漏桶算法、令牌桶算法。

漏桶算法

漏桶算法很形象,我們可以想像有一個大桶,大桶底部有一個固定大小的洞,Web請求就像水一樣,先進入大桶,然後以固定的速率從底部漏出來,無論進入桶中的水多麼迅猛,漏桶算法始終以固定的速度來漏水。

對應到Web請求就是:

  • 當桶中無水時表示當前無請求等待,可以直接處理當前的請求;
  • 當桶中有水時表示當前有請求正在等待處理,此時新來的請求也是需要進行等待處理;
  • 當桶中水已經裝滿,並且進入的速率大於漏水的速率,水就會溢出來,此時系統就會拒絕新來的請求;

令牌桶算法

令牌桶跟漏桶算法有點不一樣,令牌桶算法也有一個大桶,桶中裝的都是令牌,有一個固定的“人”在不停的往桶中放令牌,每個請求來的時候都要從桶中拿到令牌,要不然就無法進行請求操作。

  • 當沒有請求來時,桶中的令牌會越來越多,一直到桶被令牌裝滿爲止,多餘的令牌會被丟棄
  • 當請求的速率大於令牌放入桶的速率,桶中的令牌會越來越少,直止桶變空爲止,此時的請求會等待新令牌的產生

漏桶算法 VS 令牌桶算法

  • 漏桶算法是桶中有水就需要等待,桶滿就拒絕請求。而令牌桶是桶變空了需要等待令牌產生;
  • 漏桶算法漏水的速率固定,令牌桶算法往桶中放令牌的速率固定;
  • 令牌桶可以接收的瞬時流量比漏桶大,比如桶的容量爲100,令牌桶會裝滿100個令牌,當有瞬時80個併發過來時可以從桶中迅速拿到令牌進行處理,而漏桶的消費速率固定,當瞬時80個併發過來時,可能需要進行排隊等待;

 

介紹了算法,接下來我們介紹下Redis實現限流的幾種方式。

第一種:基於Redis的setNX的操作

我們在使用Redis的分佈式鎖的時候,大家都知道是依靠了setNX的指令,在CAS(Compare and swap)的操作的時候,同時給指定的key設置了過期實踐(expire),我們在限流的主要目的就是爲了在單位時間內,有且僅有N數量的請求能夠訪問我的代碼程序。所以依靠setnx可以很輕鬆的做到這方面的功能。

比如我們需要在10秒內限定20個請求,那麼我們在setnx的時候可以設置過期時間10,當請求的setnx數量達到20時候即達到了限流效果。代碼比較簡單就不做展示了。

當然這種做法的弊端是很多的,比如當統計1-10秒的時候,無法統計2-11秒之內,如果需要統計N秒內的M個請求,那麼我們的Redis中需要保持N個key等等問題。

第二種:基於Redis的數據結構zset

其實限流涉及的最主要的就是滑動窗口,上面也提到1-10怎麼變成2-11。其實也就是起始值和末端值都各+1即可。

而我們如果用Redis的list數據結構可以輕而易舉的實現該功能

我們可以將請求打造成一個zset數組,當每一次請求進來的時候,value保持唯一,可以用UUID生成,而score可以用當前時間戳表示,因爲score我們可以用來計算當前時間戳之內有多少的請求數量。而zset數據結構也提供了range方法讓我們可以很輕易的獲取到2個時間戳內有多少請求(解決了第一種方案中無法統計2-11秒的問題)。

示例代碼:

using System;
using System.Threading;
using ServiceStack.Redis;
namespace IPCounter
{
    class Program
    {
        static void Main(string[] args)
        {
            RedisClient client = new RedisClient("[email protected]:6379");
            string key = "aa";
            for (int i=0;i<100;i++)
            {
                long currentTime = ToUnixTimestampBySeconds(DateTime.Now);
              
                if (client.ContainsKey(key))
                {
                    var count = client.GetRangeFromSortedSetByHighestScore(key,currentTime-1,currentTime).Count;

                    if(count>3)
                    {
                      
                        Console.WriteLine("您的請求頻率太高了");
                        Console.ReadLine();
                    }

                    count = client.GetRangeFromSortedSetByHighestScore(key, currentTime - 60, currentTime).Count;

                    if (count > 30)
                    {
                        Console.WriteLine("您的請求頻率太高了");
                        Console.ReadLine();
                    }

                }

                string value = Guid.NewGuid().ToString();
                long score = currentTime;

                client.AddItemToSortedSet(key, value, score);

                //清除2分鐘之前的記錄
                var list = client.GetRangeFromSortedSetByHighestScore(key,0, currentTime-65);
                client.RemoveItemsFromSortedSet(key, list);
                Thread.Sleep(500);
            }

            Console.WriteLine("Hello World!");
        }

        public static long  ToUnixTimestampBySeconds(DateTime dt)
        {
            DateTimeOffset dto = new DateTimeOffset(dt);
            return dto.ToUnixTimeSeconds();
        }
    }
}

 

 

通過上述代碼可以做到滑動窗口的效果,並且能保證每N秒內至多M個請求,缺點就是zset的數據結構會越來越大。實現方式相對也是比較簡單的。

第三種:基於Redis的令牌桶算法

令牌桶算法提及到輸入速率和輸出速率,當輸出速率大於輸入速率,那麼就是超出流量限制了。

也就是說我們每訪問一次請求的時候,可以從Redis中獲取一個令牌,如果拿到令牌了,那就說明沒超出限制,而如果拿不到,則結果相反。

依靠上述的思想,我們可以結合Redis的List數據結構很輕易的做到這樣的代碼,只是簡單實現依靠List的leftPop方法來獲取令牌。

示例代碼:

// 輸出令牌
  static void LimitRequest()
        {
            RedisClient client = new RedisClient("163[email protected]:6379");
            string key = "limitRate";
          var result=  client.PopItemFromList(key);
            if(result==null)
            {
                Console.WriteLine("系統繁忙,請稍後再試");
            }
            else
            {
                Console.WriteLine("訪問成功");
            }

        }

 

 

再依靠定時任務,定時往令牌桶List中加入新的令牌(使用List的rightPush方法),當然令牌也需要唯一性,這裏還是用UUID生成令牌:

// 10S的速率往令牌桶中添加UUID,保證唯一性
 /// <summary>
        /// 比如我們速率限制是1分鐘100個,那麼就處理爲1分鐘內,桶中就只有100個令牌。
        /// </summary>
        static void AddTokenToBucket()
        {
            string key = "limitRate";
            RedisClient client = new RedisClient("[email protected]:6379");

            var count = client.GetListCount(key);
            for(var i=0;i<100-count;i++) //需要判斷原來是否還有剩餘,有則相應扣減,確保桶中只有100個令牌
            {
                client.AddItemToList(key, Guid.NewGuid().ToString());
            }
        }

綜上,代碼實現起始都不是很難,針對這些限流方式我們可以在AOP或者filter中加入以上代碼,用來做到接口的限流,最終保護系統的穩定。

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