Java實(shí)現(xiàn)限流接口的示例詳解
一、限流
為什么要進(jìn)行限流?
1.瞬時流量過高,服務(wù)被壓垮?
2.惡意用戶高頻光顧,導(dǎo)致服務(wù)器宕機(jī)?
3.消息消費(fèi)過快,導(dǎo)致數(shù)據(jù)庫壓力過大,性能下降甚至崩潰?
……
什么是限流?
限流是對某一時間窗口內(nèi)的請求數(shù)進(jìn)行限制,保持系統(tǒng)的可用性和穩(wěn)定性,防止因流量暴增而導(dǎo)致的系統(tǒng)運(yùn)行緩慢或宕機(jī)。
在高并發(fā)系統(tǒng)中,出于系統(tǒng)保護(hù)角度考慮,通常會對流量進(jìn)行限流。
在分布式系統(tǒng)中,高并發(fā)場景下,為了防止系統(tǒng)因突然的流量激增而導(dǎo)致的崩潰,同時保證服務(wù)的高可用性和穩(wěn)定性,限流是最常用的手段。
有哪些限流算法?
常見的四種限流算法,分別是:固定窗口算法、滑動窗口算法、漏桶算法、令牌桶算法。
二、限流算法
固定窗口
實(shí)現(xiàn)原理
固定窗口又稱固定窗口(又稱計(jì)數(shù)器算法,F(xiàn)ixed Window)限流算法,是最簡單的限流算法。
實(shí)現(xiàn)原理:在指定周期內(nèi)累加訪問次數(shù),當(dāng)訪問次數(shù)達(dá)到設(shè)定的閾值時,觸發(fā)限流策略,當(dāng)進(jìn)入下一個時間周期時進(jìn)行訪問次數(shù)的清零。如圖所示,我們要求3秒內(nèi)的請求不要超過150次:

代碼實(shí)現(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)缺點(diǎn)
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,容易理解
缺點(diǎn):
1.限流不夠平滑。例如:限流是每秒3個,在第一毫秒發(fā)送了3個請求,達(dá)到限流,窗口剩余時間的請求都將會被拒絕,體驗(yàn)不好。
2.無法處理窗口邊界問題。因?yàn)槭窃谀硞€時間窗口內(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次請求,那么其實(shí)在第2秒到第4秒這兩秒內(nèi),就已經(jīng)發(fā)生了300次請求了,遠(yuǎn)遠(yuǎn)大于我們要求的3秒內(nèi)的請求不要超過150次這個限制,如下圖所示:

滑動窗口
實(shí)現(xiàn)原理
滑動窗口為固定窗口的改良版,解決了固定窗口在窗口切換時會受到兩倍于閾值數(shù)量的請求。在滑動窗口算法中,窗口的起止時間是動態(tài)的,窗口的大小固定。這種算法能夠較好地處理窗口邊界問題,但是實(shí)現(xiàn)相對復(fù)雜,需要記錄每個請求的時間戳。
實(shí)現(xiàn)原理:滑動窗口在固定窗口的基礎(chǔ)上,將時間窗口進(jìn)行了更精細(xì)的分片,將一個窗口分為若干個等份的小窗口,每次僅滑動一小塊的時間。每個小窗口對應(yīng)不同的時間點(diǎn),擁有獨(dú)立的計(jì)數(shù)器,當(dāng)請求的時間點(diǎn)大于當(dāng)前窗口的最大時間點(diǎn)時,則將窗口向前平移一個小窗口(將第一個小窗口的數(shù)據(jù)舍棄,第二個小窗口變成第一個小窗口,當(dāng)前請求放在最后一個小窗口),整個窗口的所有請求數(shù)相加不能大于閾值。其中,Sentinel就是采用滑動窗口算法來實(shí)現(xiàn)限流的。如圖所示:

核心步驟:
1.把3秒鐘劃分為3個小窗,每個小窗限制請求不能超過50秒。
2.比如我們設(shè)置,3秒內(nèi)不能超過150個請求,那么這個窗口就可以容納3個小窗,并且隨著時間推移,往前滑動。每次請求過來后,都要統(tǒng)計(jì)滑動窗口內(nèi)所有小窗的請求總量。
代碼實(shí)現(xiàn)
public class SlidingWindowRateLimiter {
Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class);
//時間窗口大小,單位毫秒
long windowSize;
//分片窗口數(shù)
int shardNum;
//允許通過的請求數(shù)
int maxRequestCount;
//各個窗口內(nèi)請求計(jì)數(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)缺點(diǎn)
優(yōu)點(diǎn):解決了固定窗口算法的窗口邊界問題,避免突發(fā)流量壓垮服務(wù)器。
缺點(diǎn):還是存在限流不夠平滑的問題。例如:限流是每秒3個,在第一毫秒發(fā)送了3個請求,達(dá)到限流,剩余窗口時間的請求都將會被拒絕,體驗(yàn)不好。
漏桶算法
實(shí)現(xiàn)原理
漏桶限流算法是一種常用的流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制數(shù)據(jù)的傳輸速率以及防止網(wǎng)絡(luò)擁塞。
主要的作用:
a.控制數(shù)據(jù)注入網(wǎng)絡(luò)的速度。
b.平滑網(wǎng)絡(luò)上的突發(fā)流量
實(shí)現(xiàn)原理:
漏桶是一個很形象的比喻,外部請求就像是水一樣不斷注入水桶中,而水桶已經(jīng)設(shè)置好了最大出水速率,漏桶會以這個速率勻速放行請求,而當(dāng)水超過桶的最大容量后則被丟棄。不管上面的水流速度有多塊,漏桶水滴的流出速度始終保持不變。消息中間件就采用的漏桶限流的思想。如圖所示:

核心步驟:
a.一個固定容量的漏桶,按照固定速率出水(處理請求);
b.當(dāng)流入水(請求數(shù)量)的速度過大會直接溢出(請求數(shù)量超過限制則直接拒絕)。
c.桶里的水(請求)不夠則無法出水(桶內(nèi)沒有請求則不處理)。
代碼實(shí)現(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() {
//桶中沒有水, 重新開始計(jì)算
if (water.get() == 0) {
logger.info("start leaking");
leakTimestamp = System.currentTimeMillis();
water.incrementAndGet();
return water.get() < capacity;
}
//先漏水,計(jì)算剩余水量
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)缺點(diǎn)
優(yōu)點(diǎn):
1.平滑流量。由于漏桶算法以固定的速率處理請求,可以有效地平滑和整形流量,避免流量的突發(fā)和波動(類似于消息隊(duì)列的削峰填谷的作用)。
2.防止過載。當(dāng)流入的請求超過桶的容量時,可以直接丟棄請求,防止系統(tǒng)過載。
缺點(diǎn):
1.無法處理突發(fā)流量:由于漏桶的出口速度是固定的,無法處理突發(fā)流量。例如,即使在流量較小的時候,也無法以更快的速度處理請求。
2.可能會丟失數(shù)據(jù):如果入口流量過大,超過了桶的容量,那么就需要丟棄部分請求。在一些不能接受丟失請求的場景中,這可能是一個問題。
3.不適合速率變化大的場景:如果速率變化大,或者需要動態(tài)調(diào)整速率,那么漏桶算法就無法滿足需求。
4.資源利用率:不管當(dāng)前系統(tǒng)的負(fù)載壓力如何,所有請求都得進(jìn)行排隊(duì),即使此時服務(wù)器的負(fù)載處于相對空閑的狀態(tài),這樣會造成系統(tǒng)資源的浪費(fèi)。
由于漏桶的缺陷比較明顯,所以在實(shí)際業(yè)務(wù)場景中,使用的比較少。
令牌算法
實(shí)現(xiàn)原理
令牌桶算法是基于漏桶算法的一種改進(jìn),主要在于令牌桶算法能夠在限制服務(wù)調(diào)用的平均速率的同時,還能夠允許一定程度內(nèi)的突發(fā)調(diào)用。
實(shí)現(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ù)器,因?yàn)橥皟?nèi)令牌數(shù)量是有限制的。
如圖所示:

代碼實(shí)現(xiàn)
Guava中的RateLimiter就是基于令牌桶實(shí)現(xiàn)的,可以直接拿來使用。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
1.可以處理突發(fā)流量:令牌桶算法可以處理突發(fā)流量。當(dāng)桶滿時,能夠以最大速度處理請求。這對于需要處理突發(fā)流量的應(yīng)用場景非常有用。
2.限制平均速率:在長期運(yùn)行中,數(shù)據(jù)的傳輸率會被限制在預(yù)定義的平均速率(即生成令牌的速率)。
3.靈活性:與漏桶算法相比,令牌桶算法提供了更大的靈活性。例如,可以動態(tài)地調(diào)整生成令牌的速率。
缺點(diǎn):
1.可能導(dǎo)致過載:如果令牌產(chǎn)生的速度過快,可能會導(dǎo)致大量的突發(fā)流量,這可能會使網(wǎng)絡(luò)或服務(wù)過載。
2.需要存儲空間:令牌桶需要一定的存儲空間來保存令牌,可能會導(dǎo)致內(nèi)存資源的浪費(fèi)。
3.實(shí)現(xiàn)稍復(fù)雜:相比于計(jì)數(shù)器算法,令牌桶算法的實(shí)現(xiàn)稍微復(fù)雜一些。
三、應(yīng)用實(shí)踐
Guava中的RateLimiter就是基于令牌桶實(shí)現(xiàn)的,可以直接拿來使用。所有整個實(shí)踐是基于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能夠很好的實(shí)現(xiàn)單機(jī)的限流。
同時產(chǎn)生多個令牌
那么再回到我們前面提到的突發(fā)流量情況,令牌桶是怎么解決的呢?RateLimiter中引入了一個預(yù)消費(fèi)的概念。
申請令牌的數(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秒是毫無意義的浪費(fèi)資源行為,那么可以先允許它執(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ā)放。
總的來說,基于令牌桶實(shí)現(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切面實(shí)現(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;
//驗(yàn)證緩存是否有命中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é)
本文介紹的實(shí)現(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實(shí)現(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-01
java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07
SpringBoot+MinIO+KKFileView實(shí)現(xiàn)文件預(yù)覽功能
本文主要介紹了使用SpringBoot、MinIO和KKFileView實(shí)現(xiàn)文件上傳和在線預(yù)覽功能,通過配置MinIO存儲文件,并使用KKFileView生成預(yù)覽鏈接,感興趣的可以了解一下2024-11-11
簡單了解Java synchronized關(guān)鍵字同步
這篇文章主要介紹了簡單了解Java synchronized關(guān)鍵字同步,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09
spring profile 多環(huán)境配置管理詳解
這篇文章主要介紹了 spring profile 多環(huán)境配置管理詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01
解決spring-data-jpa 事物中修改屬性自動更新update問題
這篇文章主要介紹了解決spring-data-jpa 事物中修改屬性自動更新update問題,具有很好的參考價值,希望對大家2021-08-08
SpringBoot通過Filter實(shí)現(xiàn)整個項(xiàng)目接口的SQL注入攔截詳解
這篇文章主要介紹了SpringBoot通過Filter實(shí)現(xiàn)整個項(xiàng)目接口的SQL注入攔截詳解,SQL注入是比較常見的網(wǎng)絡(luò)攻擊方式之一,在客戶端在向服務(wù)器發(fā)送請求的時候,sql命令通過表單提交或者url字符串拼接傳遞到后臺持久層,最終達(dá)到欺騙服務(wù)器執(zhí)行惡意的SQL命令,需要的朋友可以參考下2023-12-12

