【高並發】如何實現億級流量下的分布式限流?這些算法你必須掌握!!


寫在前面

在互聯網應用中,高並發系統會面臨一個重大的挑戰,那就是大量流高並發訪問,比如:天貓的雙十一、京東618、秒殺、搶購促銷等,這些都是典型的大流量高並發場景。關於秒殺,小伙伴們可以參見我的另一篇文章《【高並發】高並發秒殺系統架構解密,不是所有的秒殺都是秒殺!

關於【冰河技術】微信公眾號,解鎖更多【高並發】專題文章。

注意:由於原文篇幅比較長,所以被拆分為:理論、算法、實戰(HTTP接口實戰+分布式限流實戰)三大部分。理論篇參見《【高並發】如何實現億級流量下的分布式限流?這些理論你必須掌握!!

計數器

計數器法

限流算法中最簡單粗暴的一種算法,例如,某一個接口1分鍾內的請求不超過60次,我們可以在開始時設置一個計數器,每次請求時,這個計數器的值加1,如果這個這個計數器的值大於60並且與第一次請求的時間間隔在1分鍾之內,那么說明請求過多;如果該請求與第一次請求的時間間隔大於1分鍾,並且該計數器的值還在限流范圍內,那么重置該計數器。

使用計數器還可以用來限制一定時間內的總並發數,比如數據庫連接池、線程池、秒殺的並發數;計數器限流只要一定時間內的總請求數超過設定的閥值則進行限流,是一種簡單粗暴的總數量限流,而不是平均速率限流。

在這里插入圖片描述

這個方法有一個致命問題:臨界問題——當遇到惡意請求,在0:59時,瞬間請求100次,並且在1:00請求100次,那么這個用戶在1秒內請求了200次,用戶可以在重置節點突發請求,而瞬間超過我們設置的速率限制,用戶可能通過算法漏洞擊垮我們的應用。

在這里插入圖片描述

這個問題我們可以使用滑動窗口解決。

滑動窗口

在這里插入圖片描述

在上圖中,整個紅色矩形框是一個時間窗口,在我們的例子中,一個時間窗口就是1分鍾,然后我們將時間窗口進行划分,如上圖我們把滑動窗口划分為6格,所以每一格代表10秒,每超過10秒,我們的時間窗口就會向右滑動一格,每一格都有自己獨立的計數器,例如:一個請求在0:35到達, 那么0:30到0:39的計數器會+1,那么滑動窗口是怎么解決臨界點的問題呢?如上圖,0:59到達的100個請求會在灰色區域格子中,而1:00到達的請求會在紅色格子中,窗口會向右滑動一格,那么此時間窗口內的總請求數共200個,超過了限定的100,所以此時能夠檢測出來觸發了限流。回頭看看計數器算法,會發現,其實計數器算法就是窗口滑動算法,只不過計數器算法沒有對時間窗口進行划分,所以是一格。

由此可見,當滑動窗口的格子划分越多,限流的統計就會越精確。

漏桶算法

算法的思路就是水(請求)先進入到漏桶里面,漏桶以恆定的速度流出,當水流的速度過大就會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。如下圖所示。

在這里插入圖片描述

漏桶算法不支持突發流量。

令牌桶算法

在這里插入圖片描述

從上圖中可以看出,令牌算法有點復雜,桶里存放着令牌token。桶一開始是空的,token以固定的速率r往桶里面填充,直到達到桶的容量,多余的token會被丟棄。每當一個請求過來時,就會嘗試着移除一個token,如果沒有token,請求無法通過。

令牌桶算法支持突發流量。

令牌桶算法實現

Guava框架提供了令牌桶算法的實現,可直接使用這個框架的RateLimiter類創建一個令牌桶限流器,比如:每秒放置的令牌桶的數量為5,那么RateLimiter對象可以保證1秒內不會放入超過5個令牌,並且以固定速率進行放置令牌,達到平滑輸出的效果。

平滑流量示例

這里,我寫了一個使用Guava框架實現令牌桶算法的示例,如下所示。

package io.binghe.limit.guava;

import com.google.common.util.concurrent.RateLimiter;

/**
 * @author binghe
 * @version 1.0.0
 * @description 令牌桶算法
 */
public class TokenBucketLimiter {
    public static void main(String[] args){
        //每秒鍾生成5個令牌
        RateLimiter limiter = RateLimiter.create(5);

        //返回值表示從令牌桶中獲取一個令牌所花費的時間,單位是秒
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
    }
}

代碼的實現非常簡單,就是使用Guava框架的RateLimiter類生成了一個每秒向桶中放入5個令牌的對象,然后不斷從桶中獲取令牌。我們先來運行下這段代碼,輸出的結果信息如下所示。

0.0
0.197294
0.191278
0.19997
0.199305
0.200472
0.200184
0.199417
0.200111
0.199759

從輸出結果可以看出:第一次從桶中獲取令牌時,返回的時間為0.0,也就是沒耗費時間。之后每次從桶中獲取令牌時,都會耗費一定的時間,這是為什么呢?按理說,向桶中放入了5個令牌后,再從桶中獲取令牌也應該和第一次一樣並不會花費時間啊!

因為在Guava的實現是這樣的:我們使用RateLimiter.create(5)創建令牌桶對象時,表示每秒新增5個令牌,1秒等於1000毫秒,也就是每隔200毫秒向桶中放入一個令牌。

當我們運行程序時,程序運行到RateLimiter limiter = RateLimiter.create(5);時,就會向桶中放入一個令牌,當程序運行到第一個System.out.println(limiter.acquire(1));時,由於桶中已經存在一個令牌,直接獲取這個令牌,並沒有花費時間。然而程序繼續向下執行時,由於程序會每隔200毫秒向桶中放入一個令牌,所以,獲取令牌時,花費的時間幾乎都是200毫秒左右。

突發流量示例

我們再來看一個突發流量的示例,代碼示例如下所示。

package io.binghe.limit.guava;

import com.google.common.util.concurrent.RateLimiter;

/**
 * @author binghe
 * @version 1.0.0
 * @description 令牌桶算法
 */
public class TokenBucketLimiter {
    public static void main(String[] args){
        //每秒鍾生成5個令牌
        RateLimiter limiter = RateLimiter.create(5);

        //返回值表示從令牌桶中獲取一個令牌所花費的時間,單位是秒
        System.out.println(limiter.acquire(50));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
    }
}

上述代碼表示的含義為:每秒向桶中放入5個令牌,第一次從桶中獲取50個令牌,也就是我們說的突發流量,后續每次從桶中獲取5個令牌。接下來,我們運行上述代碼看下效果。

0.0
9.998409
0.99109
1.000148
0.999752

運行代碼時,會發現當命令行打印出0.0后,會等很久才會打印出后面的輸出結果。

程序每秒鍾向桶中放入5個令牌,當程序運行到 RateLimiter limiter = RateLimiter.create(5); 時,就會向桶中放入令牌。當運行到 System.out.println(limiter.acquire(50)); 時,發現很快就會獲取到令牌,花費了0.0秒。接下來,運行到第一個System.out.println(limiter.acquire(5));時,花費了9.998409秒。小伙們可以思考下,為什么這里會花費10秒中的時間呢?

這是因為我們使用RateLimiter limiter = RateLimiter.create(5);代碼向桶中放入令牌時,一秒鍾放入5個,而System.out.println(limiter.acquire(50));需要獲取50個令牌,也就是獲取50個令牌需要花費10秒鍾時間,這是因為程序向桶中放入50個令牌需要10秒鍾。程序第一次從桶中獲取令牌時,很快就獲取到了。而第二次獲取令牌時,花費了將近10秒的時間。

Guava框架支持突發流量,但是在突發流量之后再次請求時,會被限速,也就是說:在突發流量之后,再次請求時,會彌補處理突發請求所花費的時間。所以,我們的突發示例程序中,在一次從桶中獲取50個令牌后,再次從桶中獲取令牌,則會花費10秒左右的時間。

Guava令牌桶算法的特點

  • RateLimiter使用令牌桶算法,會進行令牌的累積,如果獲取令牌的頻率比較低,則不會導致等待,直接獲取令牌。
  • RateLimiter由於會累積令牌,所以可以應對突發流量。也就是說如果同時請求5個令牌,由於此時令牌桶中有累積的令牌,能夠快速響應請求。
  • RateLimiter在沒有足夠的令牌發放時,采用的是滯后的方式進行處理,也就是前一個請求獲取令牌所需要等待的時間由下一次請求來承受和彌補,也就是代替前一個請求進行等待。(這里,小伙伴們要好好理解下)

重磅福利

關注「 冰河技術 」微信公眾號,后台回復 “設計模式” 關鍵字領取《深入淺出Java 23種設計模式》PDF文檔。回復“Java8”關鍵字領取《Java8新特性教程》PDF文檔。兩本PDF均是由冰河原創並整理的超硬核教程,面試必備!!

好了,今天就聊到這兒吧!別忘了點個贊,給個在看和轉發,讓更多的人看到,一起學習,一起進步!!

寫在最后

如果你覺得冰河寫的還不錯,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習高並發、分布式、微服務、大數據、互聯網和雲原生技術,「 冰河技術 」微信公眾號更新了大量技術專題,每一篇技術文章干貨滿滿!不少讀者已經通過閱讀「 冰河技術 」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨干!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公眾號吧,每天更新超硬核技術干貨,讓你對如何提升技術能力不再迷茫!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM