Java代碼實現(xiàn)四種限流算法詳細(xì)介紹
前言
上個月做了一個BI項目并且使用了限流算法,目的是為了限制用戶瘋狂調(diào)用AI生成接口造成財產(chǎn)損失,畢竟AI的調(diào)用是要馬內(nèi)的。這個篇文章會仔細(xì)探討介紹四種常見的限流算法及其Java代碼實現(xiàn)。
固定窗口限流
將單位時間內(nèi)作為一個時間窗口,同時維護(hù)一個計數(shù)器,記錄次時間窗口內(nèi)接收的請求次數(shù)。
- 當(dāng)次數(shù)小于等于限流閾值,允許請求通過,并且計數(shù)器 +1
- 當(dāng)次數(shù)大于限流閾值,不允許請求通過,計數(shù)器不變
- 當(dāng)系統(tǒng)時間(當(dāng)前時間)超過時間窗口,計數(shù)器重置為0并且開始記錄新的窗口內(nèi)接收的請求次數(shù)
缺陷:定義時間窗口為1s,允許請求數(shù)量為3(請求閾值),說明1s內(nèi)只能接收并處理3個請求。然而如果0.8~0.9s來了三個請求,1.1~1.2s來了三個請求。雖然這6個請求都沒超過各自窗口的請求閾值,但是已經(jīng)違背了1s內(nèi)接收并處理3個請求了,因為在不到1s內(nèi)其接收了6個請求,存在臨界問題。
優(yōu)點:簡單易實現(xiàn)
缺點:
- 有臨界問題(流量突刺)
- 不適用于分布式系統(tǒng)
Java代碼實現(xiàn)
public class FixedWindowRateLimiter { private final int requestLimit; // 限流閾值 private final long windowSize; // 時間窗口大?。ê撩耄? private int count; // 計數(shù)器 private volatile long windowStart; // 時間窗口起始時間 public FixedWindowRateLimiter(long windowSize, int requestLimit) { this.windowSize = windowSize; this.requestLimit = requestLimit; this.count = 0; this.windowStart = System.currentTimeMillis() / 1000; } public synchronized boolean tryAcquire() { long currentTime = System.currentTimeMillis(); if (currentTime - windowStart > windowSize) { // 如果當(dāng)前時間已經(jīng)超出窗口時間,則重置窗口 windowStart = currentTime; count = 0; } if (count < requestLimit) { count++; // 增加計數(shù) return true; } else { return false; } } public static void main(String[] args) { FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(10, 2); // 時間窗口大小,請求閾值(請求限制) for (int i = 0;i < 10;i++) { boolean allowed = fixedWindowRateLimiter.tryAcquire(); System.out.println("請求" + (i+1) + ":" + (allowed ? "被允許" : "被拒絕")); } } }
結(jié)果:
滑動窗口限流
為了解決固定窗口限流的臨界問題,便有了滑動窗口限流算法。
滑動窗口將單位時間劃分n個小周期,通過計數(shù)器記錄每個小周期內(nèi)的請求次數(shù),每過0.2s后時間窗口就往后推移1小格即一個小周期(0.2s),那么最初的小周期就會被刪除,并且隨著時間的推移刪除已過期的小周期。
比如,將單位時間1s劃分5個小周期,每個小周期為0.2s。每個小周期內(nèi)的請求次數(shù)由各自且獨立的計數(shù)器記錄,統(tǒng)計5個小周期的計數(shù)器總和是否超過請求閾值,沒有就請求允許,否則請求被拒絕。起初滑動窗口是0~1s;當(dāng)過了0.2s后,0~0.2s這個小周期就會被刪除,同時滑動窗口后移0.2s(一個小周期),那么新的滑動窗口變?yōu)榱?.2~1.2s。
用上述固定窗口的臨界例子應(yīng)用到滑動窗口:0.8~0.9來了三個請求,這三個請求被允許了(此時滑動窗口是0~1.0s)。過了0.2s后,新的滑動窗口變?yōu)榱?.2~1.2s,當(dāng)1.1~1.2s來了三個請求,這三個請求就會被拒絕。因為此時新的5個小周期的計數(shù)器總和為3達(dá)到了請求閾值,所以.1~1.2s來的三個請求會被拒絕。
優(yōu)點:解決了固定限流算法的臨界(流量突刺)問題
缺點:
- 相比固定限流難理解和實現(xiàn)
- 不適用分布式系統(tǒng)
- 超過請求閾值的請求直接被拒絕了,而不是丟棄,給用戶的體驗不好
- 限流效果與劃分的小周期的數(shù)量有關(guān),但往往很難選取
Java實現(xiàn)
public class SlidingWindowRateLimiter { private final int windowSize; // 時間窗口大小,單位為秒 private final int requestLimit; // 在時間窗口內(nèi)允許的最大請求數(shù) private final LinkedList<Long> timestamps; // 存儲請求的時間戳 public SlidingWindowRateLimiter(int windowSize, int requestLimit) { this.windowSize = windowSize; this.requestLimit = requestLimit; this.timestamps = new LinkedList<>(); } public synchronized boolean allowRequest() { long currentTime = System.currentTimeMillis() / 1000; // 獲取當(dāng)前時間戳(秒) // 移除時間窗口之外的時間戳 while (!timestamps.isEmpty() && timestamps.getFirst() <= currentTime - windowSize) { timestamps.removeFirst(); } // 檢查當(dāng)前請求數(shù)是否超過限制 if (timestamps.size() < requestLimit) { timestamps.addLast(currentTime); return true; // 允許請求 } else { return false; // 請求超過限制 } } public static void main(String[] args) { SlidingWindowRateLimiter rateLimiter = new SlidingWindowRateLimiter(10, 2); // 模擬請求 for (int i = 0; i < 10; i++) { boolean allowed = rateLimiter.allowRequest(); System.out.println("請求 " + (i + 1) + ": " + (allowed ? "被允許" : "被拒絕")); } } }
結(jié)果:
漏桶限流
漏桶限流算法又算是解決了固定窗口限流和滑動窗口限流出現(xiàn)的請求被拒絕的問題,其對于超出請求閾值的請求會直接丟棄。
水(請求)以任意的速率流入桶內(nèi),以固定的速率從桶底流出(處理請求),如果水的流入速度大于流出速度(系統(tǒng)請求速度大于處理請求的速度),水就會溢出(即請求直接被丟棄)。
優(yōu)點:
- 解決了請求被直接拒絕的問題
- 一定程度上解決了臨界問題(流量突刺)
缺點:不能迅速處理一批請求(并發(fā)處理請求),因為水的流出速率是固定的(請求的處理速率是固定的,只能處理一個請求后再去處理下一個)
Java實現(xiàn):
package DataStructure.RateLimtAlgorithm; import java.time.Instant; import java.util.concurrent.atomic.AtomicLong; public class LeakyBucketRateLimiter { private final int capacity; // 桶的容量 private final int rate; // 漏水速率,單位:請求/秒 private AtomicLong water; // 當(dāng)前桶中的水量 private Instant lastLeakTime; // 上一次漏水的時間點 public LeakyBucketRateLimiter(int capacity, int rate) { this.capacity = capacity; this.rate = rate; this.water = new AtomicLong(0); this.lastLeakTime = Instant.now(); } public synchronized boolean allowed() { leak(); // 漏水 if (water.get() < capacity) { water.incrementAndGet(); return true; // 桶未滿,允許請求通過 } else { return false; // 桶已滿,拒絕請求 } } private synchronized void leak() { Instant now = Instant.now(); long interval = now.getEpochSecond() - lastLeakTime.getEpochSecond(); long leakedWater = interval * rate; // 計算漏水的數(shù)量 if (leakedWater > 0) { water.set(Math.max(0, water.get() - leakedWater)); // 更新桶中的水量 lastLeakTime = now; // 更新上一次漏水的時間點 } } public static void main(String[] args) { LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(5, 2); // 創(chuàng)建一個容量為10,速率為1個請求/秒的漏桶 for (int i = 0; i < 10; i++) { boolean allowed = leakyBucketRateLimiter.allowed(); System.out.println("請求 " + (i + 1) + ": " + (allowed ? "被允許" : "被拒絕")); try { Thread.sleep(100); // 模擬請求間隔 } catch (InterruptedException e) { e.printStackTrace(); } } } }
結(jié)果:
令牌桶限流
根據(jù)請求的數(shù)量多少,管理員勻速生成一批令牌。只有擁有令牌的請求才會被允許,否則就會被丟棄,不予通過。比如管理員生成了5個令牌,此時來了10個請求,那么拿到令牌的5個請求會被同時處理,另外5個請求就會被丟棄。
一般令牌桶限流可以通過Redisson限流器實現(xiàn),因此其適用于分布式系統(tǒng),當(dāng)然前面三種限流算也可用于分布式系統(tǒng)但是需要手動實現(xiàn)。既然令牌桶限流算法這么好,Redisson還幫我們實現(xiàn)了,所以一般我們都會選擇它,因為專業(yè) [doge]。
優(yōu)點:以上三種算法的所有優(yōu)點,并且支持并發(fā)處理請求。
缺點:
- 不適合長時間限流
- 對資源消耗高,依賴Redis實現(xiàn)
- 不適用于突發(fā)性任務(wù)
Java實現(xiàn):
@Service public class RedisLimiterManager { @Resource private RedissonClient redissonClient; /** * 限流操作 * * @param key 區(qū)分不同的限流器,比如不同的用戶 id 應(yīng)該分別統(tǒng)計 */ public void doRateLimit(String key) { // 創(chuàng)建一個名稱為user_limiter的限流器,每秒最多訪問2次 RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS); // OVERALL類型:不管有多少臺服務(wù)器都是放在一起統(tǒng)計的,請求閾值,生成令牌的頻率 // 每當(dāng)一個操作來了后,請求一個令牌 boolean canOp = rateLimiter.tryAcquire(1);// 令牌請求數(shù),處理請求需消耗的令牌 if (!canOp) { throw new BusinessException(ErrorCode.TOO_MANY_REQUEST); } } } @SpringBootTest class RedisLimiterManagerTest { @Resource private RedisLimiterManager redisLimiterManager; @Test void doRateLimit() throws InterruptedException { String userId = "1"; for (int i=0;i<10;i++) { redisLimiterManager.doRateLimit(userId); System.out.println("success"); } } }
結(jié)果:
參考來源:面試必備:4種經(jīng)典限流算法講解 - 掘金
到此這篇關(guān)于Java代碼實現(xiàn)四種限流算法詳細(xì)介紹的文章就介紹到這了,更多相關(guān)Java 限流算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Java兩個浮點型數(shù)字加減乘除的問題
由于浮點數(shù)在計算機(jī)中是以二進(jìn)制表示的,直接進(jìn)行加減乘除運算會出現(xiàn)精度誤差,想要得到精確結(jié)果,應(yīng)使用BigDecimal類進(jìn)行運算2024-10-10SpringBoot 如何根據(jù)不同profile選擇不同配置
這篇文章主要介紹了SpringBoot 如何根據(jù)不同profile選擇不同配置的操作方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08SpringBoot整合Springsecurity實現(xiàn)數(shù)據(jù)庫登錄及權(quán)限控制功能
本教程詳細(xì)介紹了如何使用SpringBoot整合SpringSecurity實現(xiàn)數(shù)據(jù)庫登錄和權(quán)限控制,本文分步驟結(jié)合實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-10-10基于Java堆內(nèi)存的10個要點的總結(jié)分析
本篇文章是對Java堆內(nèi)存的10個要點進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05Java split函數(shù)拆分后變成null問題解決方案
這篇文章主要介紹了Java split函數(shù)拆分后變成null問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10