Guava Ratelimit源碼分析以及仿造優化版本

今天介紹Guava的限流RateLimit,主要介紹2個,一個是源碼分析,一個是仿造他的原理寫一個優化版本。

一、源碼分析

首先我個人認爲RateLimit的設計思想很好很優秀,但是寫的有點瑕疵,後面我會說到。

①create方法

第一個參數是Guava的秒錶工具用來計時的,第二個參數是你輸入的,也就是每秒生成的令牌數。SmoothBursty是RateLimit的子類,看看他的構造方法。

第二個參數默認值爲1,官方意思是當ratelimit不工作時候可以保存多少秒的標籤,其實我覺得真的是不清不楚的,網上很多人也都直接來過來粘貼進去。其實這個意思就是這個參數是用來計算你的桶的大小用的,你create時候只輸入了每秒產生多少個,乘以這個參數就等於桶大小,也就是說默認方法是每秒產生多少個 == 桶size。

setRate方法就是校驗一下參數然後加鎖調用doSetRate方法,然後你就可以看到我上面說的maxBurstSeconds參數的作用了。storedPermits是當前桶內令牌數,由於我們舒適化時候都沒對maxPermits賦值,所以這個方法最終storedPermits會賦0。

這樣create就完成了,這裏我只介紹普通模式,熱啓動不做介紹,其實也差不多。

②tryAcquired

第一個參數是每次消費多少個令牌,第二個是獲取不到令牌時候延時多久放棄,第三個是時間工具類。可以看出來最重要其實就是canAcquire和reserveAndGetWaitLength方法,接下來我們逐一介紹着兩個方法。

1、canAcquire

queryEarliestAvailable方法是獲取 最近一個可以獲得的令牌的時間(這個話聽上去有點繞,沒事有個概念即可,後面會詳細介紹)。那麼canAcquire方法用公式表示就是:最近可以獲得令牌時間 <= 當前時間 + 超時時間。

2、reserveAndGetWaitLength 次方法是時間Ratelimit的核心中的核心,講的比較多,公式也比較多,耐心看完!

reserveAndGetWaitLength 調用了reserveEarliestAvailable方法,看看裏頭實現了什麼

首先第一個參數是你這次請求需要多少個令牌,第二個是當前時間。然後第一步調用了一個resync同步方法,看看這個方法

這裏有幾個成員變量,我先解釋一下什麼意思。

nextFreeTicketMicros:距離下一個可以獲得令牌的時間,如果當前時間大於它,那麼至於當前時間。

storedPermits:當前桶裏令牌數

maxPermits:桶大小

permitsPerSecond:每秒生成的令牌數

stableIntervalMicros:生成一個令牌需要的時間,單位是微秒,計算公式是:1000000 * 1s / permitsPerSecond

這個方法,就是計算當開始生成令牌的時間到現在這段時間生成了多少令牌,這個也是Guava跟一般令牌桶實現不一樣的地方,它並不是用線程異步放令牌,而是通過計算時間差而得出。這樣做有一個好處就是,如果我現在要針對每個接口限流,我需要每個接口創建一個桶,如果用異步線程生產令牌,那要開多少個線程。

然後把桶最大值和計算出的令牌數對比,選一個小的賦給當前令牌數參數。並且把當前時間給nextFreeTicketMicros。

回到上一個方法。

同步完成後,比較需要的令牌和當前桶裏剩餘令牌,並且做差值。storePermitsToWaitTime方法在普通限流時候都是返回0,所以不用看。然後計算等待時間,公式是:(需要的令牌數 - 當前剩餘令牌數)* 每個令牌生成時間。nextFreeTicketMicros此時由於上面調用過resync方法,所以現在是當前時間所以nextFreeTicketMicros = nowMics + waitMIcros;最後減少桶內令牌即可。

看到這裏就問你懵不懵!,尤其是nextFreeTicketMicros這個參數,其實你回到最早我們canacquire方法

這裏queryEarliestAvailable方法就是獲取nextFreeTicketMicros,來比較時間,看時候有令牌。而nextFreeTicketMicros是由當前時間加上等待時間算出來的,也就是說這個參數是說下一個生成令牌的時間,如果當前時間小於這個參數,那麼就是說令牌還沒生成所以就返回false。

二、優化

代碼我貼出來了,我簡化了很多操作,但是性能還是跟Guava是一樣的(做過壓測),唯一要注意的點,就是計算當前令牌數的時候。

@Slf4j
public class RateLimit {
    //當前桶裏ticket數量
    private double currentTicketNum;
    //桶大小
    private long bucketSize;
    //距離下一個可獲得的ticket的時間
    private long nextFreeTicketMic;
    //每秒生成的ticket數量
    private double ticketPerSecond;
    private double lastConsumMic;


    private Stopwatch stopwatch;
    private Lock lock;

    //創建
    public static RateLimit create(long ticketPerSecond) {
        if (ticketPerSecond < 1) {
            throw new IllegalArgumentException("param must > 1");
        }

        RateLimit rateLimit = new RateLimit();
        rateLimit.bucketSize = ticketPerSecond;
        rateLimit.ticketPerSecond = ticketPerSecond;
        rateLimit.stopwatch = Stopwatch.createStarted();
        rateLimit.lock = new ReentrantLock();

        return rateLimit;
    }

    //非阻塞獲取ticket,該方法只允許一次獲取一個ticket並且不允許提前消費
    public boolean tryAcquire() {
        try {
            lock.lock();
            long nowMic = this.stopwatch.elapsed(MICROSECONDS);
            //判斷如果當前時間小於可以獲得ticket時間直接返回false
            if (this.nextFreeTicketMic > nowMic) {
                return false;
            } else {
                //計算這一段時間產生的ticket數量,你發現沒我這裏用的不是nextFreeTicketMic而是lastConsumMic,
                //確實用作者寫的沒問題,但是它算出來的總比實際的當前令牌數少1個,所以這裏做了優化
                this.currentTicketNum = Math.min(bucketSize, currentTicketNum + (nowMic - lastConsumMic) / buildTicketPerMic());

                //該次請求消耗ticket
                if (currentTicketNum > 0) {
                    currentTicketNum--;
                }

                long waitMic = 0L;
                if (currentTicketNum <= 0) {
                    waitMic = (long) buildTicketPerMic();
                }

                //計算下一次可以獲取到ticket的時間
                lastConsumMic = nowMic;
                this.nextFreeTicketMic = nowMic + waitMic;
            }
        } catch (Exception e) {
            log.error("獲取ticket異常", e);
        } finally {
            lock.unlock();
        }

        return true;
    }

    private double buildTicketPerMic() {
        return SECONDS.toMicros(1L) / ticketPerSecond;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章