欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java實現(xiàn)限流接口的示例詳解

 更新時間:2023年12月21日 10:01:17   作者:myprince003  
限流是對某一時間窗口內(nèi)的請求數(shù)進(jìn)行限制,保持系統(tǒng)的可用性和穩(wěn)定性,防止因流量暴增而導(dǎo)致的系統(tǒng)運行緩慢或宕機(jī),本文主要來和大家聊聊如何使用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實現(xiàn)銀行家算法

    使用java實現(xiàn)銀行家算法

    這篇文章主要為大家詳細(xì)介紹了如何使用java實現(xiàn)銀行家算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-12-12
  • 淺談java Iterator.remove()方法的用法(詳解)

    淺談java Iterator.remove()方法的用法(詳解)

    下面小編就為大家?guī)硪黄獪\談java Iterator.remove()方法的用法(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01
  • java實現(xiàn)學(xué)生信息管理系統(tǒng)

    java實現(xiàn)學(xué)生信息管理系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • SpringBoot+MinIO+KKFileView實現(xiàn)文件預(yù)覽功能

    SpringBoot+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)鍵字同步

    這篇文章主要介紹了簡單了解Java synchronized關(guān)鍵字同步,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-09-09
  • Tomcat ClassLoader打破雙親委派源碼解析

    Tomcat ClassLoader打破雙親委派源碼解析

    這篇文章主要為大家介紹了Tomcat ClassLoader打破雙親委派源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Java的基礎(chǔ)語法學(xué)習(xí)筆記

    Java的基礎(chǔ)語法學(xué)習(xí)筆記

    這里為大家整理了Java的基礎(chǔ)語法學(xué)習(xí)筆記,包括關(guān)鍵詞、運算符與基本的流程控制語句寫法等,需要的朋友可以參考下
    2016-05-05
  • spring profile 多環(huán)境配置管理詳解

    spring profile 多環(huán)境配置管理詳解

    這篇文章主要介紹了 spring profile 多環(huán)境配置管理詳解的相關(guān)資料,需要的朋友可以參考下
    2017-01-01
  • 解決spring-data-jpa 事物中修改屬性自動更新update問題

    解決spring-data-jpa 事物中修改屬性自動更新update問題

    這篇文章主要介紹了解決spring-data-jpa 事物中修改屬性自動更新update問題,具有很好的參考價值,希望對大家
    2021-08-08
  • SpringBoot通過Filter實現(xiàn)整個項目接口的SQL注入攔截詳解

    SpringBoot通過Filter實現(xiàn)整個項目接口的SQL注入攔截詳解

    這篇文章主要介紹了SpringBoot通過Filter實現(xiàn)整個項目接口的SQL注入攔截詳解,SQL注入是比較常見的網(wǎng)絡(luò)攻擊方式之一,在客戶端在向服務(wù)器發(fā)送請求的時候,sql命令通過表單提交或者url字符串拼接傳遞到后臺持久層,最終達(dá)到欺騙服務(wù)器執(zhí)行惡意的SQL命令,需要的朋友可以參考下
    2023-12-12

最新評論