面試必備:四種經(jīng)典限流算法講解
前言
大家好,我是田螺。
最近一位朋友去拼夕夕面試,被問(wèn)了這么一道題:限流算法有哪些?用代碼實(shí)現(xiàn)令牌桶算法。跟星球好友討論了一波,發(fā)現(xiàn)大家都忘記得差不多了.所以田螺哥再整理一波,常見(jiàn)的四種限流算法,以及簡(jiǎn)單代碼實(shí)現(xiàn),相信大家看完,會(huì)茅塞頓開(kāi)的。
圖片
1. 固定窗口限流算法
1.1 什么是固定窗口限流算法
固定窗口限流算法(Fixed Window Rate Limiting Algorithm)是一種最簡(jiǎn)單的限流算法,其原理是在固定時(shí)間窗口(單位時(shí)間)內(nèi)限制請(qǐng)求的數(shù)量。該算法將時(shí)間分成固定的窗口,并在每個(gè)窗口內(nèi)限制請(qǐng)求的數(shù)量。具體來(lái)說(shuō),算法將請(qǐng)求按照時(shí)間順序放入時(shí)間窗口中,并計(jì)算該時(shí)間窗口內(nèi)的請(qǐng)求數(shù)量,如果請(qǐng)求數(shù)量超出了限制,則拒絕該請(qǐng)求。
假設(shè)單位時(shí)間(固定時(shí)間窗口)是1秒,限流閥值為3。在單位時(shí)間1秒內(nèi),每來(lái)一個(gè)請(qǐng)求,計(jì)數(shù)器就加1,如果計(jì)數(shù)器累加的次數(shù)超過(guò)限流閥值3,后續(xù)的請(qǐng)求全部拒絕。等到1s結(jié)束后,計(jì)數(shù)器清0,重新開(kāi)始計(jì)數(shù)。如下圖:
圖片
1.2 固定窗口限流的偽代碼
public static Integer counter = 0; //統(tǒng)計(jì)請(qǐng)求數(shù)
public static long lastAcquireTime = 0L;
public static final Long windowUnit = 1000L ; //假設(shè)固定時(shí)間窗口是1000ms
public static final Integer threshold = 10; // 窗口閥值是10
/**
* 固定窗口時(shí)間算法
* 關(guān)注公眾號(hào):撿田螺的小男孩
* @return
*/
public synchronized boolean fixedWindowsTryAcquire() {
long currentTime = System.currentTimeMillis(); //獲取系統(tǒng)當(dāng)前時(shí)間
if (currentTime - lastAcquireTime > windowUnit) { //檢查是否在時(shí)間窗口內(nèi)
counter = 0; // 計(jì)數(shù)器清0
lastAcquireTime = currentTime; //開(kāi)啟新的時(shí)間窗口
}
if (counter < threshold) { // 小于閥值
counter++; //計(jì)數(shù)統(tǒng)計(jì)器加1
return true;
}
return false;
}
1.2 固定窗口算法的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):固定窗口算法非常簡(jiǎn)單,易于實(shí)現(xiàn)和理解。
- 缺點(diǎn):存在明顯的臨界問(wèn)題,比如: 假設(shè)限流閥值為5個(gè)請(qǐng)求,單位時(shí)間窗口是1s,如果我們?cè)趩挝粫r(shí)間內(nèi)的前0.8-1s和1-1.2s,分別并發(fā)5個(gè)請(qǐng)求。雖然都沒(méi)有超過(guò)閥值,但是如果算0.8-1.2s,則并發(fā)數(shù)高達(dá)10,已經(jīng)超過(guò)單位時(shí)間1s不超過(guò)5閥值的定義啦。
圖片
2. 滑動(dòng)窗口限流算法
2.1 什么是滑動(dòng)窗口限流算法
滑動(dòng)窗口限流算法是一種常用的限流算法,用于控制系統(tǒng)對(duì)外提供服務(wù)的速率,防止系統(tǒng)被過(guò)多的請(qǐng)求壓垮。它將單位時(shí)間周期分為n個(gè)小周期,分別記錄每個(gè)小周期內(nèi)接口的訪問(wèn)次數(shù),并且根據(jù)時(shí)間滑動(dòng)刪除過(guò)期的小周期。它可以解決固定窗口臨界值的問(wèn)題。
用一張圖解釋滑動(dòng)窗口算法,如下:
圖片
假設(shè)單位時(shí)間還是1s,滑動(dòng)窗口算法把它劃分為5個(gè)小周期,也就是滑動(dòng)窗口(單位時(shí)間)被劃分為5個(gè)小格子。每格表示0.2s。每過(guò)0.2s,時(shí)間窗口就會(huì)往右滑動(dòng)一格。然后呢,每個(gè)小周期,都有自己獨(dú)立的計(jì)數(shù)器,如果請(qǐng)求是0.83s到達(dá)的,0.8~1.0s對(duì)應(yīng)的計(jì)數(shù)器就會(huì)加1。
我們來(lái)看下,滑動(dòng)窗口,去解決固定窗口限流算法的臨界問(wèn)題,思想是怎樣
假設(shè)我們1s內(nèi)的限流閥值還是5個(gè)請(qǐng)求,0.8~1.0s內(nèi)(比如0.9s的時(shí)候)來(lái)了5個(gè)請(qǐng)求,落在黃色格子里。時(shí)間過(guò)了1.0s這個(gè)點(diǎn)之后,又來(lái)5個(gè)請(qǐng)求,落在紫色格子里。如果是固定窗口算法,是不會(huì)被限流的,但是滑動(dòng)窗口的話,每過(guò)一個(gè)小周期,它會(huì)右移一個(gè)小格。過(guò)了1.0s這個(gè)點(diǎn)后,會(huì)右移一小格,當(dāng)前的單位時(shí)間段是0.2~1.2s,這個(gè)區(qū)域的請(qǐng)求已經(jīng)超過(guò)限定的5了,已觸發(fā)限流啦,實(shí)際上,紫色格子的請(qǐng)求都被拒絕啦。
當(dāng)滑動(dòng)窗口的格子周期劃分的越多,那么滑動(dòng)窗口的滾動(dòng)就越平滑,限流的統(tǒng)計(jì)就會(huì)越精確。
2.2 滑動(dòng)窗口限流算法的偽代碼實(shí)現(xiàn)
/**
* 單位時(shí)間劃分的小周期(單位時(shí)間是1分鐘,10s一個(gè)小格子窗口,一共6個(gè)格子)
*/
private int SUB_CYCLE = 10;
/**
* 每分鐘限流請(qǐng)求數(shù)
*/
private int thresholdPerMin = 100;
/**
* 計(jì)數(shù)器, k-為當(dāng)前窗口的開(kāi)始時(shí)間值秒,value為當(dāng)前窗口的計(jì)數(shù)
*/
private final TreeMap<Long, Integer> counters = new TreeMap<>();
/**
* 滑動(dòng)窗口時(shí)間算法實(shí)現(xiàn)
*/
public synchronized boolean slidingWindowsTryAcquire() {
long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //獲取當(dāng)前時(shí)間在哪個(gè)小周期窗口
int currentWindowNum = countCurrentWindow(currentWindowTime); //當(dāng)前窗口總請(qǐng)求數(shù)
//超過(guò)閥值限流
if (currentWindowNum >= thresholdPerMin) {
return false;
}
//計(jì)數(shù)器+1
counters.get(currentWindowTime)++;
return true;
}
/**
* 統(tǒng)計(jì)當(dāng)前窗口的請(qǐng)求數(shù)
*/
private int countCurrentWindow(long currentWindowTime) {
//計(jì)算窗口開(kāi)始位置
long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
int count = 0;
//遍歷存儲(chǔ)的計(jì)數(shù)器
Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = iterator.next();
// 刪除無(wú)效過(guò)期的子窗口計(jì)數(shù)器
if (entry.getKey() < startTime) {
iterator.remove();
} else {
//累加當(dāng)前窗口的所有計(jì)數(shù)器之和
count =count + entry.getValue();
}
}
return count;
}
2.3 滑動(dòng)窗口限流算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 簡(jiǎn)單易懂
- 精度高(通過(guò)調(diào)整時(shí)間窗口的大小來(lái)實(shí)現(xiàn)不同的限流效果)
- 可擴(kuò)展性強(qiáng)(可以非常容易地與其他限流算法結(jié)合使用)
缺點(diǎn):
- 突發(fā)流量無(wú)法處理(無(wú)法應(yīng)對(duì)短時(shí)間內(nèi)的大量請(qǐng)求,但是一旦到達(dá)限流后,請(qǐng)求都會(huì)直接暴力被拒絕。醬紫我們會(huì)損失一部分請(qǐng)求,這其實(shí)對(duì)于產(chǎn)品來(lái)說(shuō),并不太友好),需要合理調(diào)整時(shí)間窗口大小。
3. 漏桶限流算法
3.1 什么是漏桶限流算法
漏桶限流算法(Leaky Bucket Algorithm)是一種流量控制算法,用于控制流入網(wǎng)絡(luò)的數(shù)據(jù)速率,以防止網(wǎng)絡(luò)擁塞。它的思想是將數(shù)據(jù)包看作是水滴,漏桶看作是一個(gè)固定容量的水桶,數(shù)據(jù)包像水滴一樣從桶的頂部流入桶中,并通過(guò)桶底的一個(gè)小孔以一定的速度流出,從而限制了數(shù)據(jù)包的流量。
漏桶限流算法的基本工作原理是:對(duì)于每個(gè)到來(lái)的數(shù)據(jù)包,都將其加入到漏桶中,并檢查漏桶中當(dāng)前的水量是否超過(guò)了漏桶的容量。如果超過(guò)了容量,就將多余的數(shù)據(jù)包丟棄。如果漏桶中還有水,就以一定的速率從桶底輸出數(shù)據(jù)包,保證輸出的速率不超過(guò)預(yù)設(shè)的速率,從而達(dá)到限流的目的。
圖片
- 流入的水滴,可以看作是訪問(wèn)系統(tǒng)的請(qǐng)求,這個(gè)流入速率是不確定的。
- 桶的容量一般表示系統(tǒng)所能處理的請(qǐng)求數(shù)。
- 如果桶的容量滿了,就達(dá)到限流的閥值,就會(huì)丟棄水滴(拒絕請(qǐng)求)
- 流出的水滴,是恒定過(guò)濾的,對(duì)應(yīng)服務(wù)按照固定的速率處理請(qǐng)求。
3.2 漏桶限流算法的偽代碼實(shí)現(xiàn)
/**
* LeakyBucket 類(lèi)表示一個(gè)漏桶,
* 包含了桶的容量和漏桶出水速率等參數(shù),
* 以及當(dāng)前桶中的水量和上次漏水時(shí)間戳等狀態(tài)。
*/
public class LeakyBucket {
private final long capacity; // 桶的容量
private final long rate; // 漏桶出水速率
private long water; // 當(dāng)前桶中的水量
private long lastLeakTimestamp; // 上次漏水時(shí)間戳
public LeakyBucket(long capacity, long rate) {
this.capacity = capacity;
this.rate = rate;
this.water = 0;
this.lastLeakTimestamp = System.currentTimeMillis();
}
/**
* tryConsume() 方法用于嘗試向桶中放入一定量的水,如果桶中還有足夠的空間,則返回 true,否則返回 false。
* @param waterRequested
* @return
*/
public synchronized boolean tryConsume(long waterRequested) {
leak();
if (water + waterRequested <= capacity) {
water += waterRequested;
return true;
} else {
return false;
}
}
/**
* 。leak() 方法用于漏水,根據(jù)當(dāng)前時(shí)間和上次漏水時(shí)間戳計(jì)算出應(yīng)該漏出的水量,然后更新桶中的水量和漏水時(shí)間戳等狀態(tài)。
*/
private void leak() {
long now = System.currentTimeMillis();
long elapsedTime = now - lastLeakTimestamp;
long leakedWater = elapsedTime * rate / 1000;
if (leakedWater > 0) {
water = Math.max(0, water - leakedWater);
lastLeakTimestamp = now;
}
}
}
- 注意: tryConsume() 和 leak() 方法中,都需要對(duì)桶的狀態(tài)進(jìn)行同步,以保證線程安全性。
3.3 漏桶限流算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 可以平滑限制請(qǐng)求的處理速度,避免瞬間請(qǐng)求過(guò)多導(dǎo)致系統(tǒng)崩潰或者雪崩。
- 可以控制請(qǐng)求的處理速度,使得系統(tǒng)可以適應(yīng)不同的流量需求,避免過(guò)載或者過(guò)度閑置。
- 可以通過(guò)調(diào)整桶的大小和漏出速率來(lái)滿足不同的限流需求,可以靈活地適應(yīng)不同的場(chǎng)景。
缺點(diǎn)
- 需要對(duì)請(qǐng)求進(jìn)行緩存,會(huì)增加服務(wù)器的內(nèi)存消耗。
- 對(duì)于流量波動(dòng)比較大的場(chǎng)景,需要較為靈活的參數(shù)配置才能達(dá)到較好的效果。
- 但是面對(duì)突發(fā)流量的時(shí)候,漏桶算法還是循規(guī)蹈矩地處理請(qǐng)求,這不是我們想看到的啦。流量變突發(fā)時(shí),我們肯定希望系統(tǒng)盡量快點(diǎn)處理請(qǐng)求,提升用戶體驗(yàn)嘛。
4. 令牌桶算法
4.1 什么是令牌桶算法
令牌桶算法是一種常用的限流算法,可以用于限制單位時(shí)間內(nèi)請(qǐng)求的數(shù)量。該算法維護(hù)一個(gè)固定容量的令牌桶,每秒鐘會(huì)向令牌桶中放入一定數(shù)量的令牌。當(dāng)有請(qǐng)求到來(lái)時(shí),如果令牌桶中有足夠的令牌,則請(qǐng)求被允許通過(guò)并從令牌桶中消耗一個(gè)令牌,否則請(qǐng)求被拒絕。
4.2 令牌桶算法的偽代碼實(shí)現(xiàn)
/**
* TokenBucket 類(lèi)表示一個(gè)令牌桶
*/
public class TokenBucket {
private final int capacity; // 令牌桶容量
private final int rate; // 令牌生成速率,單位:令牌/秒
private int tokens; // 當(dāng)前令牌數(shù)量
private long lastRefillTimestamp; // 上次令牌生成時(shí)間戳
/**
* 構(gòu)造函數(shù)中傳入令牌桶的容量和令牌生成速率。
* @param capacity
* @param rate
*/
public TokenBucket(int capacity, int rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
/**
* allowRequest() 方法表示一個(gè)請(qǐng)求是否允許通過(guò),該方法使用 synchronized 關(guān)鍵字進(jìn)行同步,以保證線程安全。
* @return
*/
public synchronized boolean allowRequest() {
refill();
if (tokens > 0) {
tokens--;
return true;
} else {
return false;
}
}
/**
* refill() 方法用于生成令牌,其中計(jì)算令牌數(shù)量的邏輯是按照令牌生成速率每秒鐘生成一定數(shù)量的令牌,
* tokens 變量表示當(dāng)前令牌數(shù)量,
* lastRefillTimestamp 變量表示上次令牌生成的時(shí)間戳。
*/
private void refill() {
long now = System.currentTimeMillis();
if (now > lastRefillTimestamp) {
int generatedTokens = (int) ((now - lastRefillTimestamp) / 1000 * rate);
tokens = Math.min(tokens + generatedTokens, capacity);
lastRefillTimestamp = now;
}
}
}
4.3 令牌桶算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 穩(wěn)定性高:令牌桶算法可以控制請(qǐng)求的處理速度,可以使系統(tǒng)的負(fù)載變得穩(wěn)定。
- 精度高:令牌桶算法可以根據(jù)實(shí)際情況動(dòng)態(tài)調(diào)整生成令牌的速率,可以實(shí)現(xiàn)較高精度的限流。
- 彈性好:令牌桶算法可以處理突發(fā)流量,可以在短時(shí)間內(nèi)提供更多的處理能力,以處理突發(fā)流量。
Guava的RateLimiter限流組件,就是基于令牌桶算法實(shí)現(xiàn)的。
缺點(diǎn):
- 實(shí)現(xiàn)復(fù)雜:相對(duì)于固定窗口算法等其他限流算法,令牌桶算法的實(shí)現(xiàn)較為復(fù)雜。對(duì)短時(shí)請(qǐng)求難以處理:在短時(shí)間內(nèi)有大量請(qǐng)求到來(lái)時(shí),可能會(huì)導(dǎo)致令牌桶中的令牌被快速消耗完,從而限流。這種情況下,可以考慮使用漏桶算法。
- 時(shí)間精度要求高:令牌桶算法需要在固定的時(shí)間間隔內(nèi)生成令牌,因此要求時(shí)間精度較高,如果系統(tǒng)時(shí)間不準(zhǔn)確,可能會(huì)導(dǎo)致限流效果不理想。
總體來(lái)說(shuō),令牌桶算法具有較高的穩(wěn)定性和精度,但實(shí)現(xiàn)相對(duì)復(fù)雜,適用于對(duì)穩(wěn)定性和精度要求較高的場(chǎng)景。