java限流算法詳細(xì)
1、場(chǎng)景
程序中經(jīng)常需要對(duì)接口進(jìn)行限流,防止訪(fǎng)問(wèn)量太大,導(dǎo)致程序崩潰。
常用的算法有:計(jì)數(shù)算法、
漏桶算法、令牌桶算法,最常用的算法是后面兩種。
2、算法詳解
2.1 計(jì)數(shù)算法
2.1.1 說(shuō)明
技術(shù)算法,為最簡(jiǎn)單的限流算法。
核心思想是,每隔一段時(shí)間,為計(jì)數(shù)器設(shè)定最大值,請(qǐng)求一次,計(jì)數(shù)器數(shù)量減一,如果計(jì)數(shù)器為0,則拒絕請(qǐng)求。
計(jì)數(shù)器算法圖示:

2.1.2 適用場(chǎng)景
雖然此算法是大多數(shù)人第一個(gè)想到可以限流的算法,但是不推薦使用此算法。
因?yàn)椋怂惴ㄓ袀€(gè)致命性的問(wèn)題,如果1秒允許的訪(fǎng)問(wèn)次數(shù)為100,前0.99秒內(nèi)沒(méi)有任何請(qǐng)求,在最后0.01秒內(nèi),出現(xiàn)了200個(gè)請(qǐng)求,則這200個(gè)請(qǐng)求,都會(huì)獲取調(diào)用許可,給程序帶來(lái)一次請(qǐng)求的高峰。
如下圖所示:計(jì)數(shù)器算法缺點(diǎn)

2.1.3 代碼
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
/**
 * 計(jì)數(shù)器限流器
 */
public class CountLimiter {
    /**
     * 執(zhí)行區(qū)間(毫秒)
     */
    private int secondMill;
    
    /**
     * 區(qū)間內(nèi)計(jì)數(shù)多少次
     */
    private int maxCount;
    
    /**
     * 當(dāng)前計(jì)數(shù)
     */
    private int currentCount;
    
    /**
     * 上次更新時(shí)間(毫秒)
     */
    private long lastUpdateTime;
    
    public CountLimiter(int second, int count) {
        if (second <= 0 || count <= 0) {
            throw new IllegalArgumentException("second and time must by positive");
        }
        this.secondMill = second * 1000;
        this.maxCount = count;
        this.currentCount = this.maxCount;
        this.lastUpdateTime = System.currentTimeMillis();
    }
    
    /**
     * 刷新計(jì)數(shù)器
     */
    private void refreshCount() {
        long now = System.currentTimeMillis();
        if ((now - this.lastUpdateTime) >= secondMill) {
            this.currentCount = maxCount;
            this.lastUpdateTime = now;
        }
    }
    
    /**
     * 獲取授權(quán)
     * @return
     */
    public synchronized boolean tryAcquire() {
        // 刷新計(jì)數(shù)器
        this.refreshCount();
        if ((this.currentCount - 1) >= 0) {
            this.currentCount--;
            return true;
        } else {
            return false;
        }
    }
}
測(cè)試方法:
public static void main(String[] args) throws Exception {
    // 1秒限制執(zhí)行2次
    CountLimiter countLimiter = new CountLimiter(1, 2);
    for (int i = 0; i < 10; i++) {
        System.out.println(LocalDateTime.now() + " " + countLimiter.tryAcquire());
        TimeUnit.MILLISECONDS.sleep(200);
    }
}
執(zhí)行結(jié)果:
2021-05-31T22:01:08.660 true 2021-05-31T22:01:08.868 true 2021-05-31T22:01:09.074 false 2021-05-31T22:01:09.275 false 2021-05-31T22:01:09.485 false 2021-05-31T22:01:09.698 true 2021-05-31T22:01:09.901 true 2021-05-31T22:01:10.104 false 2021-05-31T22:01:10.316 false 2021-05-31T22:01:10.520 false
2.2 漏桶算法
2.2.1 說(shuō)明
漏桶算法稱(chēng)為leaky bucket,可限制指定時(shí)間內(nèi)的最大流量,如限制60秒內(nèi),最多允許100個(gè)請(qǐng)求。
其中接受請(qǐng)求的速度是不恒定的(水滴入桶),處理請(qǐng)求的速度是恒定的(水滴出桶)。
算法總體描述如下:
- 有個(gè)固定容量的桶B(指定時(shí)間區(qū)間X,允許的的最大流量B),如60秒內(nèi)最多允許100個(gè)請(qǐng)求,則B為100,X為60。
 - 有水滴流進(jìn)來(lái)(有請(qǐng)求進(jìn)來(lái)),桶里的水+1。
 - 有水滴流出去(執(zhí)行請(qǐng)求對(duì)應(yīng)的業(yè)務(wù)),桶里的水-1
(業(yè)務(wù)方法,真正開(kāi)始執(zhí)行=>這是保證漏桶勻速處理業(yè)務(wù)的根本),水滴流出去的速度是勻速的,流速為B/X(1毫秒100/60次,約1毫秒0.00167次,精度可根據(jù)實(shí)際情況自己控制) - 水桶滿(mǎn)了后(60秒內(nèi)請(qǐng)求達(dá)到了100次),水滴無(wú)法進(jìn)入水桶,請(qǐng)求被拒絕
 
2.2.2 漏桶算法圖示
實(shí)際開(kāi)發(fā)中,漏桶的使用方式可參考下圖:

注意:水滴滴落的時(shí)候,才開(kāi)始執(zhí)行業(yè)務(wù)代碼,而不是水滴進(jìn)桶的時(shí)候,去執(zhí)行業(yè)務(wù)代碼。
業(yè)務(wù)代碼的執(zhí)行方式,個(gè)人認(rèn)為有如下兩種:
同步執(zhí)行:
- 調(diào)用方請(qǐng)求時(shí),如水滴可以放入桶中,調(diào)用方所在的線(xiàn)程“阻塞”
 - 水滴漏出時(shí),喚醒調(diào)用方線(xiàn)程,調(diào)用方線(xiàn)程,執(zhí)行具體業(yè)務(wù)
 
異步執(zhí)行:
- 調(diào)用方請(qǐng)求時(shí),如水滴可以放入桶中,調(diào)用方所在的線(xiàn)程收到響應(yīng),方法將異步執(zhí)行
 - 水滴漏出時(shí),水桶代理執(zhí)行具體業(yè)務(wù)
 
網(wǎng)上很多滴桶的實(shí)現(xiàn)代碼,在水滴進(jìn)桶的時(shí)候,就去執(zhí)行業(yè)務(wù)代碼了。這樣會(huì)導(dǎo)致業(yè)務(wù)代碼,
無(wú)法勻速地執(zhí)行,仍然對(duì)被調(diào)用的接口有一瞬間流量的沖擊(和令牌桶算法的最終實(shí)現(xiàn)效果一樣)。
2.2.3 適用場(chǎng)景
水桶的進(jìn)水速度是不可控的,有可能一瞬間有大量的請(qǐng)求進(jìn)入水桶。處理請(qǐng)求的速度是恒定的(滴水的時(shí)候處理請(qǐng)求)。
此算法,主要應(yīng)用于自己的服務(wù),調(diào)用外部接口。以均勻的速度調(diào)用外部接口,防止對(duì)外部接口的壓力過(guò)大,而影響外部系統(tǒng)的穩(wěn)定性。如果影響了別人的系統(tǒng),接口所在公司會(huì)來(lái)找你喝茶。
漏桶算法,主要用來(lái)保護(hù)別人的接口。
2.2.4 代碼
本實(shí)例代碼的實(shí)現(xiàn),在水滴滴下,執(zhí)行具體業(yè)務(wù)代碼時(shí),采用同步執(zhí)行的方式。即喚醒調(diào)用方的線(xiàn)程,讓"調(diào)用者"所屬的線(xiàn)程去執(zhí)行具體業(yè)務(wù)代碼,去調(diào)用接口。
import java.net.SocketTimeoutException;
import java.time.LocalDateTime;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
 * 漏桶算法
 */
public class LeakyBucketLimiterUtil {
    
    /**
     * 漏桶流出速率(多少納秒執(zhí)行一次)
     */
    private long outflowRateNanos;
    
    /**
     * 漏桶容器
     */
    private volatile BlockingQueue<Drip> queue;
    
    /**
     * 滴水線(xiàn)程
     */
    private Thread outflowThread;
    
    /**
     * 水滴
     */
    private static class Drip {
        /**
         * 業(yè)務(wù)主鍵
         */
        private String busId;
        
        /**
         * 水滴對(duì)應(yīng)的調(diào)用者線(xiàn)程
         */
        private Thread thread;
        
        public Drip(String busId, Thread thread) {
            this.thread = thread;
        }
        
        public String getBusId() {
            return this.busId;
        }
        
        public Thread getThread() {
            return this.thread;
        }
    }
    
    /**
     * @param second 秒
     * @param time   調(diào)用次數(shù)
     */
    public LeakyBucketLimiterUtil(int second, int time) {
        if (second <= 0 || time <= 0) {
            throw new IllegalArgumentException("second and time must by positive");
        }
        
        outflowRateNanos = TimeUnit.SECONDS.toNanos(second) / time;
        queue = new LinkedBlockingQueue<>(time);
        
        outflowThread = new Thread(() -> {
            while (true) {
                Drip drip = null;
                try {
                    // 阻塞,直到從桶里拿到水滴
                    drip = queue.take();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (drip != null && drip.getThread() != null) {
                    // 喚醒阻塞的水滴里面的線(xiàn)程
                    LockSupport.unpark(drip.getThread());
                }
                // 休息一段時(shí)間,開(kāi)始下一次滴水
                LockSupport.parkNanos(this, outflowRateNanos);
            }
        }, "漏水線(xiàn)程");
        outflowThread.start();
    }
    
    /**
     * 業(yè)務(wù)請(qǐng)求
     *
     * @return
     */
    public boolean acquire(String busId) {
        Thread thread = Thread.currentThread();
        Drip drip = new Drip(busId, thread);
        if (this.queue.offer(drip)) {
            LockSupport.park();
            return true;
        } else {
            return false;
        }
    }
}
測(cè)試代碼如下:
public static void main(String[] args) throws Exception {
    // 1秒限制執(zhí)行1次
    LeakyBucketLimiterUtil leakyBucketLimiter = new LeakyBucketLimiterUtil(5, 2);
    for (int i = 0; i < 10; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String busId = "[業(yè)務(wù)ID:" + LocalDateTime.now().toString() + "]";
                if (leakyBucketLimiter.acquire(busId)) {
                    System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + ":調(diào)用外部接口...成功:" + busId);
                } else {
                    System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + ":調(diào)用外部接口...失敗:" + busId);
                }
            }
        }, "測(cè)試線(xiàn)程-" + i).start();
        TimeUnit.MILLISECONDS.sleep(500);
    }
}
執(zhí)行結(jié)果如下:
2021-05-31T20:52:52.297 測(cè)試線(xiàn)程-0:調(diào)用外部接口...成功:[業(yè)務(wù)ID:2021-05-31T20:52:52.295] 2021-05-31T20:52:53.782 測(cè)試線(xiàn)程-3:調(diào)用外部接口...失?。篬業(yè)務(wù)ID:2021-05-31T20:52:53.782] 2021-05-31T20:52:54.286 測(cè)試線(xiàn)程-4:調(diào)用外部接口...失?。篬業(yè)務(wù)ID:2021-05-31T20:52:54.286] 2021-05-31T20:52:54.799 測(cè)試線(xiàn)程-1:調(diào)用外部接口...成功:[業(yè)務(wù)ID:2021-05-31T20:52:52.761] 2021-05-31T20:52:55.300 測(cè)試線(xiàn)程-6:調(diào)用外部接口...失?。篬業(yè)務(wù)ID:2021-05-31T20:52:55.300] 2021-05-31T20:52:55.806 測(cè)試線(xiàn)程-7:調(diào)用外部接口...失?。篬業(yè)務(wù)ID:2021-05-31T20:52:55.806] 2021-05-31T20:52:56.307 測(cè)試線(xiàn)程-8:調(diào)用外部接口...失敗:[業(yè)務(wù)ID:2021-05-31T20:52:56.307] 2021-05-31T20:52:56.822 測(cè)試線(xiàn)程-9:調(diào)用外部接口...失?。篬業(yè)務(wù)ID:2021-05-31T20:52:56.822] 2021-05-31T20:52:57.304 測(cè)試線(xiàn)程-2:調(diào)用外部接口...成功:[業(yè)務(wù)ID:2021-05-31T20:52:53.271] 2021-05-31T20:52:59.817 測(cè)試線(xiàn)程-5:調(diào)用外部接口...成功:[業(yè)務(wù)ID:2021-05-31T20:52:54.799]
2.3 令牌桶算法
2.3.1 說(shuō)明
令牌桶算法,主要是勻速地增加可用令牌,令牌數(shù)因?yàn)橥暗南拗朴袛?shù)量上限。
請(qǐng)求拿到令牌,相當(dāng)于拿到授權(quán),即可進(jìn)行相應(yīng)的業(yè)務(wù)操作。
2.3.2 令牌桶算法圖示

2.3.3 適用場(chǎng)景
和漏桶算法比,有可能導(dǎo)致短時(shí)間內(nèi)的請(qǐng)求數(shù)上升(因?yàn)槟玫搅钆坪?,就可以訪(fǎng)問(wèn)接口,有可能一瞬間將所有令牌拿走),但是不會(huì)有計(jì)數(shù)算法那樣高的峰值(因?yàn)榱钆茢?shù)量是勻速增加的)。
一般自己調(diào)用自己的接口,接口會(huì)有一定的伸縮性,令牌桶算法,主要用來(lái)保護(hù)自己的服務(wù)器接口。
2.3.4 代碼
代碼實(shí)現(xiàn)如下:
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
/**
 * 令牌桶限流算法
 */
public class TokenBucketLimiter {
    
    /**
     * 桶的大小
     */
    private double bucketSize;
    
    /**
     * 桶里的令牌數(shù)
     */
    private double tokenCount;
    
    /**
     * 令牌增加速度(每毫秒)
     */
    private double tokenAddRateMillSecond;
    
    /**
     * 上次更新時(shí)間(毫秒)
     */
    private long lastUpdateTime;
    
    /**
     * @param second 秒
     * @param time   調(diào)用次數(shù)
     */
    public TokenBucketLimiter(double second, double time) {
        if (second <= 0 || time <= 0) {
            throw new IllegalArgumentException("second and time must by positive");
        }
        // 桶的大小
        this.bucketSize = time;
        // 桶里的令牌數(shù)
        this.tokenCount = this.bucketSize;
        // 令牌增加速度(每毫秒)
        this.tokenAddRateMillSecond = time / second / 1000;
        // 上次更新時(shí)間(毫秒)
        this.lastUpdateTime = System.currentTimeMillis();
    }
    
    /**
     * 刷新桶內(nèi)令牌數(shù)(令牌數(shù)不得超過(guò)桶的大小)
     * 計(jì)算“上次刷新時(shí)間”到“當(dāng)前刷新時(shí)間”中間,增加的令牌數(shù)
     */
    private void refreshTokenCount() {
        long now = System.currentTimeMillis();
        this.tokenCount = Math.min(this.bucketSize, this.tokenCount + ((now - this.lastUpdateTime) * this.tokenAddRateMillSecond));
        this.lastUpdateTime = now;
    }
    
    /**
     * 嘗試拿到權(quán)限
     *
     * @return
     */
    public synchronized boolean tryAcquire() {
        // 刷新桶內(nèi)令牌數(shù)
        this.refreshTokenCount();
        if ((this.tokenCount - 1) >= 0) {
            // 如果桶中有令牌,令牌數(shù)-1
            this.tokenCount--;
            return true;
        } else {
            // 桶中已無(wú)令牌
            return false;
        }
    }
}
測(cè)試代碼:
public static void main(String[] args) throws Exception{
    // 2秒執(zhí)行1次
    TokenBucketLimiter leakyBucketLimiter = new TokenBucketLimiter(2, 1);
    for (int i = 0; i < 10; i++) {
        System.out.println(LocalDateTime.now() + " " + leakyBucketLimiter.tryAcquire());
        TimeUnit.SECONDS.sleep(1);
    }
}
執(zhí)行結(jié)果如下:
2021-05-31T21:38:34.560 true 2021-05-31T21:38:35.582 false 2021-05-31T21:38:36.588 true 2021-05-31T21:38:37.596 false 2021-05-31T21:38:38.608 true 2021-05-31T21:38:39.610 false 2021-05-31T21:38:40.615 true 2021-05-31T21:38:41.627 false 2021-05-31T21:38:42.641 true 2021-05-31T21:38:43.649 false
2.3.5 第三方工具類(lèi)
可以使用Guava中的RateLimiter來(lái)實(shí)現(xiàn)令牌桶的限流功能。
maven依賴(lài)如下:
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>
直接獲取令牌(true為獲取到令牌,false為獲取失?。?/strong>
RateLimiter rateLimiter = RateLimiter.create(2);
boolean acquireResule = rateLimiter.tryAcquire();
if (acquireResule) {
    System.out.println("獲取令牌:成功");
} else {
    System.out.println("獲取令牌:失敗");
}
等待嘗試獲取令牌(阻塞當(dāng)前線(xiàn)程,直到獲取到令牌):
RateLimiter rateLimiter = RateLimiter.create(2);
// 阻塞獲取令牌
double waitCount = rateLimiter.acquire();
System.out.println("阻塞等待時(shí)間:" + waitCount);
以上就是java限流算法詳細(xì)的詳細(xì)內(nèi)容,更多關(guān)于java限流算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!希望大家以后多多支持腳本之家!
相關(guān)文章
 SpringMVC對(duì)日期類(lèi)型的轉(zhuǎn)換示例
本篇文章主要介紹了SpringMVC對(duì)日期類(lèi)型的轉(zhuǎn)換示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
 性能爆棚的實(shí)體轉(zhuǎn)換復(fù)制工具M(jìn)apStruct使用詳解
這篇文章主要為大家介紹了性能爆棚的實(shí)體轉(zhuǎn)換復(fù)制工具M(jìn)apStruct使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
 在java中判斷兩個(gè)浮點(diǎn)型(float)數(shù)據(jù)是否相等的案例
這篇文章主要介紹了在java中判斷兩個(gè)浮點(diǎn)型(float)數(shù)據(jù)是否相等的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
 Pattern.compile函數(shù)提取字符串中指定的字符(推薦)
這篇文章主要介紹了Pattern.compile函數(shù)提取字符串中指定的字符,使用的是Java中的Pattern.compile函數(shù)來(lái)實(shí)現(xiàn)對(duì)指定字符串的截取,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
 Mybatis批量插入Oracle數(shù)據(jù)的方法實(shí)例
在開(kāi)發(fā)中或多或少都會(huì)遇到數(shù)據(jù)批量插入的功能,最近我在做項(xiàng)目的過(guò)程中就遇到了這樣一個(gè)問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于Mybatis批量插入Oracle數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2022-01-01
 Java從零編寫(xiě)汽車(chē)租賃系統(tǒng)全程分析
這篇文章介紹了Java實(shí)現(xiàn)汽車(chē)租賃系統(tǒng)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
 Java實(shí)現(xiàn)base64圖片編碼數(shù)據(jù)轉(zhuǎn)換為本地圖片的方法
這篇文章主要介紹了Java實(shí)現(xiàn)base64圖片編碼數(shù)據(jù)轉(zhuǎn)換為本地圖片的方法,涉及java編碼轉(zhuǎn)換及圖片文件生成相關(guān)操作技巧,需要的朋友可以參考下2018-06-06

