Java實現(xiàn)限流接口的示例詳解
一、限流
為什么要進(jìn)行限流?
1.瞬時流量過高,服務(wù)被壓垮?
2.惡意用戶高頻光顧,導(dǎo)致服務(wù)器宕機(jī)?
3.消息消費過快,導(dǎo)致數(shù)據(jù)庫壓力過大,性能下降甚至崩潰?
……
什么是限流?
限流是對某一時間窗口內(nèi)的請求數(shù)進(jìn)行限制,保持系統(tǒng)的可用性和穩(wěn)定性,防止因流量暴增而導(dǎo)致的系統(tǒng)運行緩慢或宕機(jī)。
在高并發(fā)系統(tǒng)中,出于系統(tǒng)保護(hù)角度考慮,通常會對流量進(jìn)行限流。
在分布式系統(tǒng)中,高并發(fā)場景下,為了防止系統(tǒng)因突然的流量激增而導(dǎo)致的崩潰,同時保證服務(wù)的高可用性和穩(wěn)定性,限流是最常用的手段。
有哪些限流算法?
常見的四種限流算法,分別是:固定窗口算法、滑動窗口算法、漏桶算法、令牌桶算法。
二、限流算法
固定窗口
實現(xiàn)原理
固定窗口又稱固定窗口(又稱計數(shù)器算法,F(xiàn)ixed Window)限流算法,是最簡單的限流算法。
實現(xiàn)原理:在指定周期內(nèi)累加訪問次數(shù),當(dāng)訪問次數(shù)達(dá)到設(shè)定的閾值時,觸發(fā)限流策略,當(dāng)進(jìn)入下一個時間周期時進(jìn)行訪問次數(shù)的清零。如圖所示,我們要求3秒內(nèi)的請求不要超過150次:
代碼實現(xiàn)
public class FixedWindowRateLimiter { Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class); //時間窗口大小,單位毫秒 long windowSize; //允許通過的請求數(shù) int maxRequestCount; //當(dāng)前窗口通過的請求數(shù) AtomicInteger counter = new AtomicInteger(0); //窗口右邊界 long windowBorder; public FixedWindowRateLimiter(long windowSize, int maxRequestCount) { this.windowSize = windowSize; this.maxRequestCount = maxRequestCount; this.windowBorder = System.currentTimeMillis() + windowSize; } public synchronized boolean tryAcquire() { long currentTime = System.currentTimeMillis(); if (windowBorder < currentTime) { logger.info("window reset"); do { windowBorder += windowSize; } while (windowBorder < currentTime); counter = new AtomicInteger(0); } if (counter.intValue() < maxRequestCount) { counter.incrementAndGet(); logger.info("tryAcquire success"); return true; } else { logger.info("tryAcquire fail"); return false; } } }
優(yōu)缺點
優(yōu)點:實現(xiàn)簡單,容易理解
缺點:
1.限流不夠平滑。例如:限流是每秒3個,在第一毫秒發(fā)送了3個請求,達(dá)到限流,窗口剩余時間的請求都將會被拒絕,體驗不好。
2.無法處理窗口邊界問題。因為是在某個時間窗口內(nèi)進(jìn)行流量控制,所以可能會出現(xiàn)窗口邊界效應(yīng),即在時間窗口的邊界處可能會有大量的請求被允許通過,從而導(dǎo)致突發(fā)流量。即:如果第2到3秒內(nèi)產(chǎn)生了150次請求,而第3到4秒內(nèi)產(chǎn)生了150次請求,那么其實在第2秒到第4秒這兩秒內(nèi),就已經(jīng)發(fā)生了300次請求了,遠(yuǎn)遠(yuǎn)大于我們要求的3秒內(nèi)的請求不要超過150次這個限制,如下圖所示:
滑動窗口
實現(xiàn)原理
滑動窗口為固定窗口的改良版,解決了固定窗口在窗口切換時會受到兩倍于閾值數(shù)量的請求。在滑動窗口算法中,窗口的起止時間是動態(tài)的,窗口的大小固定。這種算法能夠較好地處理窗口邊界問題,但是實現(xiàn)相對復(fù)雜,需要記錄每個請求的時間戳。
實現(xiàn)原理:滑動窗口在固定窗口的基礎(chǔ)上,將時間窗口進(jìn)行了更精細(xì)的分片,將一個窗口分為若干個等份的小窗口,每次僅滑動一小塊的時間。每個小窗口對應(yīng)不同的時間點,擁有獨立的計數(shù)器,當(dāng)請求的時間點大于當(dāng)前窗口的最大時間點時,則將窗口向前平移一個小窗口(將第一個小窗口的數(shù)據(jù)舍棄,第二個小窗口變成第一個小窗口,當(dāng)前請求放在最后一個小窗口),整個窗口的所有請求數(shù)相加不能大于閾值。其中,Sentinel就是采用滑動窗口算法來實現(xiàn)限流的。如圖所示:
核心步驟:
1.把3秒鐘劃分為3個小窗,每個小窗限制請求不能超過50秒。
2.比如我們設(shè)置,3秒內(nèi)不能超過150個請求,那么這個窗口就可以容納3個小窗,并且隨著時間推移,往前滑動。每次請求過來后,都要統(tǒng)計滑動窗口內(nèi)所有小窗的請求總量。
代碼實現(xiàn)
public class SlidingWindowRateLimiter { Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class); //時間窗口大小,單位毫秒 long windowSize; //分片窗口數(shù) int shardNum; //允許通過的請求數(shù) int maxRequestCount; //各個窗口內(nèi)請求計數(shù) int[] shardRequestCount; //請求總數(shù) int totalCount; //當(dāng)前窗口下標(biāo) int shardId; //每個小窗口大小,毫秒 long tinyWindowSize; //窗口右邊界 long windowBorder; public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) { this.windowSize = windowSize; this.shardNum = shardNum; this.maxRequestCount = maxRequestCount; this.shardRequestCount = new int[shardNum]; this.tinyWindowSize = windowSize / shardNum; this.windowBorder = System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long currentTime = System.currentTimeMillis(); if (windowBorder < currentTime) { logger.info("window reset"); do { shardId = (++shardId) % shardNum; totalCount -= shardRequestCount[shardId]; shardRequestCount[shardId] = 0; windowBorder += tinyWindowSize; } while (windowBorder < currentTime); } if (totalCount < maxRequestCount) { logger.info("tryAcquire success:{}", shardId); shardRequestCount[shardId]++; totalCount++; return true; } else { logger.info("tryAcquire fail"); return false; } } }
優(yōu)缺點
優(yōu)點:解決了固定窗口算法的窗口邊界問題,避免突發(fā)流量壓垮服務(wù)器。
缺點:還是存在限流不夠平滑的問題。例如:限流是每秒3個,在第一毫秒發(fā)送了3個請求,達(dá)到限流,剩余窗口時間的請求都將會被拒絕,體驗不好。
漏桶算法
實現(xiàn)原理
漏桶限流算法是一種常用的流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制數(shù)據(jù)的傳輸速率以及防止網(wǎng)絡(luò)擁塞。
主要的作用:
a.控制數(shù)據(jù)注入網(wǎng)絡(luò)的速度。
b.平滑網(wǎng)絡(luò)上的突發(fā)流量
實現(xiàn)原理:
漏桶是一個很形象的比喻,外部請求就像是水一樣不斷注入水桶中,而水桶已經(jīng)設(shè)置好了最大出水速率,漏桶會以這個速率勻速放行請求,而當(dāng)水超過桶的最大容量后則被丟棄。不管上面的水流速度有多塊,漏桶水滴的流出速度始終保持不變。消息中間件就采用的漏桶限流的思想。如圖所示:
核心步驟:
a.一個固定容量的漏桶,按照固定速率出水(處理請求);
b.當(dāng)流入水(請求數(shù)量)的速度過大會直接溢出(請求數(shù)量超過限制則直接拒絕)。
c.桶里的水(請求)不夠則無法出水(桶內(nèi)沒有請求則不處理)。
代碼實現(xiàn)
public class LeakyBucketRateLimiter { Logger logger = LoggerFactory.getLogger(LeakyBucketRateLimiter.class); //桶的容量 int capacity; //桶中現(xiàn)存水量 AtomicInteger water = new AtomicInteger(); //開始漏水時間 long leakTimestamp; //水流出的速率,即每秒允許通過的請求數(shù) int leakRate; public LeakyBucketRateLimiter(int capacity, int leakRate) { this.capacity = capacity; this.leakRate = leakRate; } public synchronized boolean tryAcquire() { //桶中沒有水, 重新開始計算 if (water.get() == 0) { logger.info("start leaking"); leakTimestamp = System.currentTimeMillis(); water.incrementAndGet(); return water.get() < capacity; } //先漏水,計算剩余水量 long currentTime = System.currentTimeMillis(); int leakedWater = (int) ((currentTime - leakTimestamp) / 1000 * leakRate); logger.info("lastTime:{}, currentTime:{}. LeakedWater:{}", leakTimestamp, currentTime, leakedWater); //可能時間不足,則先不漏水 if (leakedWater != 0) { int leftWater = water.get() - leakedWater; //可能水已漏光。設(shè)為0 water.set(Math.max(0, leftWater)); leakTimestamp = System.currentTimeMillis(); } logger.info("剩余容量:{}", capacity - water.get()); if (water.get() < capacity) { logger.info("tryAcquire sucess"); water.incrementAndGet(); return true; } else { logger.info("tryAcquire fail"); return false; } } }
優(yōu)缺點
優(yōu)點:
1.平滑流量。由于漏桶算法以固定的速率處理請求,可以有效地平滑和整形流量,避免流量的突發(fā)和波動(類似于消息隊列的削峰填谷的作用)。
2.防止過載。當(dāng)流入的請求超過桶的容量時,可以直接丟棄請求,防止系統(tǒng)過載。
缺點:
1.無法處理突發(fā)流量:由于漏桶的出口速度是固定的,無法處理突發(fā)流量。例如,即使在流量較小的時候,也無法以更快的速度處理請求。
2.可能會丟失數(shù)據(jù):如果入口流量過大,超過了桶的容量,那么就需要丟棄部分請求。在一些不能接受丟失請求的場景中,這可能是一個問題。
3.不適合速率變化大的場景:如果速率變化大,或者需要動態(tài)調(diào)整速率,那么漏桶算法就無法滿足需求。
4.資源利用率:不管當(dāng)前系統(tǒng)的負(fù)載壓力如何,所有請求都得進(jìn)行排隊,即使此時服務(wù)器的負(fù)載處于相對空閑的狀態(tài),這樣會造成系統(tǒng)資源的浪費。
由于漏桶的缺陷比較明顯,所以在實際業(yè)務(wù)場景中,使用的比較少。
令牌算法
實現(xiàn)原理
令牌桶算法是基于漏桶算法的一種改進(jìn),主要在于令牌桶算法能夠在限制服務(wù)調(diào)用的平均速率的同時,還能夠允許一定程度內(nèi)的突發(fā)調(diào)用。
實現(xiàn)原理:
1.系統(tǒng)以固定的速率向桶中添加令牌;
2.當(dāng)有請求到來時,會嘗試從桶中移除一個令牌,如果桶中有足夠的令牌,則請求可以被處理或數(shù)據(jù)包可以被發(fā)送;
3.如果桶中沒有令牌,那么請求將被拒絕;
4.桶中的令牌數(shù)不能超過桶的容量,如果新生成的令牌超過了桶的容量,那么新的令牌會被丟棄。
5.令牌桶算法的一個重要特性是,它能夠應(yīng)對突發(fā)流量。當(dāng)桶中有足夠的令牌時,可以一次性處理多個請求,這對于需要處理突發(fā)流量的應(yīng)用場景非常有用。但是又不會無限制的增加處理速率導(dǎo)致壓垮服務(wù)器,因為桶內(nèi)令牌數(shù)量是有限制的。
如圖所示:
代碼實現(xiàn)
Guava中的RateLimiter就是基于令牌桶實現(xiàn)的,可以直接拿來使用。
優(yōu)缺點
優(yōu)點:
1.可以處理突發(fā)流量:令牌桶算法可以處理突發(fā)流量。當(dāng)桶滿時,能夠以最大速度處理請求。這對于需要處理突發(fā)流量的應(yīng)用場景非常有用。
2.限制平均速率:在長期運行中,數(shù)據(jù)的傳輸率會被限制在預(yù)定義的平均速率(即生成令牌的速率)。
3.靈活性:與漏桶算法相比,令牌桶算法提供了更大的靈活性。例如,可以動態(tài)地調(diào)整生成令牌的速率。
缺點:
1.可能導(dǎo)致過載:如果令牌產(chǎn)生的速度過快,可能會導(dǎo)致大量的突發(fā)流量,這可能會使網(wǎng)絡(luò)或服務(wù)過載。
2.需要存儲空間:令牌桶需要一定的存儲空間來保存令牌,可能會導(dǎo)致內(nèi)存資源的浪費。
3.實現(xiàn)稍復(fù)雜:相比于計數(shù)器算法,令牌桶算法的實現(xiàn)稍微復(fù)雜一些。
三、應(yīng)用實踐
Guava中的RateLimiter就是基于令牌桶實現(xiàn)的,可以直接拿來使用。所有整個實踐是基于Guava的應(yīng)用。
引入依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency>
API直接使用
固定產(chǎn)生令牌
@Test public void acquireTest() { //每秒固定生成5個令牌 RateLimiter rateLimiter = RateLimiter.create(5); for (int i = 0; i < 10; i++) { double time = rateLimiter.acquire(); logger.info("等待時間:{}s", time); } }
結(jié)果:
可以看到,每200ms左右產(chǎn)生一個令牌并放行請求,也就是1秒放行5個請求,使用RateLimiter能夠很好的實現(xiàn)單機(jī)的限流。
同時產(chǎn)生多個令牌
那么再回到我們前面提到的突發(fā)流量情況,令牌桶是怎么解決的呢?RateLimiter中引入了一個預(yù)消費的概念。
申請令牌的數(shù)量不同不會影響這個申請令牌這個動作本身的響應(yīng)時間,acquire(1)和acquire(1000)這兩個請求會消耗同樣的時間返回結(jié)果,但是會影響下一個請求的響應(yīng)時間。
如果一個消耗大量令牌的任務(wù)到達(dá)空閑的RateLimiter,會被立即批準(zhǔn)執(zhí)行,但是當(dāng)下一個請求進(jìn)來時,將會額外等待一段時間,用來支付前一個請求的時間成本。
至于為什么要這么做,通過舉例來引申一下。當(dāng)一個系統(tǒng)處于空閑狀態(tài)時,突然來了1個需要消耗100個令牌的任務(wù),那么白白等待100秒是毫無意義的浪費資源行為,那么可以先允許它執(zhí)行,并對后續(xù)請求進(jìn)行限流時間上的延長,以此來達(dá)到一個應(yīng)對突發(fā)流量的效果。
@Test public void acquireSmoothly() { RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS); long startTimeStamp = System.currentTimeMillis(); for (int i = 0; i < 15; i++) { double time = rateLimiter.acquire(); logger.info("等待時間:{}s, 總時間:{}ms", time, System.currentTimeMillis() - startTimeStamp); } }
結(jié)果:
可以看到,令牌發(fā)放時間從最開始的500ms多逐漸縮短,在3秒后達(dá)到了200ms左右的勻速發(fā)放。
總的來說,基于令牌桶實現(xiàn)的RateLimiter功能還是非常強(qiáng)大的,在限流的基礎(chǔ)上還可以把請求平均分散在各個時間段內(nèi),因此在單機(jī)情況下它是使用比較廣泛的限流組件。
AOP切面
第一步:創(chuàng)建注解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Limit { // 資源主鍵 String key() default ""; //最多訪問次數(shù),代表請求總數(shù)量 double permitsPerSeconds(); // 時間:即timeout時間內(nèi),只允許有permitsPerSeconds個請求總數(shù)量訪問,超過的將被限制不能訪問 long timeout(); //時間類型 TimeUnit timeUnit() default TimeUnit.MILLISECONDS; //提示信息 String msg() default "系統(tǒng)繁忙,請稍后重試"; }
第二步:AOP切面實現(xiàn)
@Aspect @Component public class LimitAspect { Logger logger = LoggerFactory.getLogger(LimitAspect.class); private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); ???@Around("@annotation(com.alibaba.xxx.xxx.annotation.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { // key作用:不同的接口,不同的流量控制 String key = limit.key(); RateLimiter rateLimiter; //驗證緩存是否有命中key if (!limitMap.containsKey(key)) { //創(chuàng)建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSeconds()); limitMap.put(key, rateLimiter); logger.info("新建了令牌桶={},容量={}", key, limit.permitsPerSeconds()); } rateLimiter = limitMap.get(key); //拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeUnit()); //拿不到令牌,直接返回異常信息 if (!acquire) { logger.debug("令牌桶={},獲取令牌失敗", key); throw new RuntimeException(limit.msg()); } } return joinPoint.proceed(); } }
第三步:應(yīng)用
@Limit(key = "query", permitsPerSeconds = 1, timeout = 1, msg = "觸發(fā)接口限流,請重試")
第四步:使用位置詳解
若是放在http的mapping接口上,返回如下
{ "timestamp": "2023-12-07 11:21:47", "status": 500, "error": "Internal Server Error", "path": "/table/query" }
若是放在service服務(wù)的接口上,返回如下
{ "code": -1, "message": "觸發(fā)接口限流,請重試", "data": "fail" }
四、總結(jié)
本文介紹的實現(xiàn)方式屬于應(yīng)用級限制,應(yīng)用級限流方式只是單應(yīng)用內(nèi)的請求限流,不能進(jìn)行全局限流。假設(shè)將應(yīng)用部署到多臺機(jī)器,我們需要分布式限流和接入層限流來解決這個問題。
總的來說,要保證系統(tǒng)的抗壓能力,限流是一個必不可少的環(huán)節(jié),雖然可能會造成某些用戶的請求被丟棄,但相比于突發(fā)流量造成的系統(tǒng)宕機(jī)來說,這些損失一般都在可以接受的范圍之內(nèi)。前面也說過,限流可以結(jié)合熔斷、降級一起使用,多管齊下,保證服務(wù)的可用性與健壯性。
以上就是Java實現(xiàn)限流接口的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Java限流接口的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談java Iterator.remove()方法的用法(詳解)
下面小編就為大家?guī)硪黄獪\談java Iterator.remove()方法的用法(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01java實現(xiàn)學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07SpringBoot+MinIO+KKFileView實現(xiàn)文件預(yù)覽功能
本文主要介紹了使用SpringBoot、MinIO和KKFileView實現(xiàn)文件上傳和在線預(yù)覽功能,通過配置MinIO存儲文件,并使用KKFileView生成預(yù)覽鏈接,感興趣的可以了解一下2024-11-11簡單了解Java synchronized關(guān)鍵字同步
這篇文章主要介紹了簡單了解Java synchronized關(guān)鍵字同步,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09spring profile 多環(huán)境配置管理詳解
這篇文章主要介紹了 spring profile 多環(huán)境配置管理詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01解決spring-data-jpa 事物中修改屬性自動更新update問題
這篇文章主要介紹了解決spring-data-jpa 事物中修改屬性自動更新update問題,具有很好的參考價值,希望對大家2021-08-08SpringBoot通過Filter實現(xiàn)整個項目接口的SQL注入攔截詳解
這篇文章主要介紹了SpringBoot通過Filter實現(xiàn)整個項目接口的SQL注入攔截詳解,SQL注入是比較常見的網(wǎng)絡(luò)攻擊方式之一,在客戶端在向服務(wù)器發(fā)送請求的時候,sql命令通過表單提交或者url字符串拼接傳遞到后臺持久層,最終達(dá)到欺騙服務(wù)器執(zhí)行惡意的SQL命令,需要的朋友可以參考下2023-12-12