今天介紹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; } }