令牌桶算法及實(shí)現(xiàn)(二)

接上篇。Guava的令牌桶的實(shí)現(xiàn)中掉弛,包括一條設(shè)計(jì)哲學(xué)搅幅,需要大家注意:它允許瞬間的流量波峰超過(guò)QPS,但瞬間過(guò)后的請(qǐng)求將會(huì)等待較長(zhǎng)的時(shí)間來(lái)緩解上次的波峰堪置,以使得平均的QPS等于預(yù)定值躬存。
RateLimiter類(lèi)提供了令牌桶的接口,它是一個(gè)抽象類(lèi)舀锨,其子類(lèi)有SmoothRateLimiter(也是個(gè)抽象類(lèi))以及孫子類(lèi)SmoothBursty岭洲,SmoothWarmingUp。SmoothRateLimiter類(lèi)實(shí)現(xiàn)了算法的核心部分坎匿,因次我們暫且只討論SmoothRateLimiter和其實(shí)現(xiàn)類(lèi)SmoothBursty盾剩。RateLimiter都是通過(guò)靜態(tài)的create函數(shù)實(shí)例化雷激。以create(double permitsPerSecond)為例。參數(shù)permitsPerSecond為配置的QPS告私。該方法簡(jiǎn)潔明了侥锦,屏蔽了很多用戶無(wú)需關(guān)心的細(xì)節(jié)。

public static RateLimiter create(double permitsPerSecond) {
      return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }

接著該方法調(diào)用了create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer())方法(該方法由于是包的訪問(wèn)權(quán)限德挣,在實(shí)際的項(xiàng)目中恭垦,基本不會(huì)直接調(diào)用),同時(shí)創(chuàng)建了一個(gè)StopWatch格嗅,自動(dòng)啟動(dòng)番挺。

static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

該方法創(chuàng)建了SmoothBursty實(shí)例,up-casting為RateLimiter屯掖。maxBurstSeconds固定為1玄柏,說(shuō)明令牌桶中所能存儲(chǔ)的的最大令牌數(shù)是1*QPS。接著調(diào)用setRate方法贴铜,該方法初始化一些重要的參數(shù):

public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      doSetRate(permitsPerSecond, stopwatch.readMicros());
    }
  }

主要實(shí)現(xiàn)在SmoothRateLimiter中:

@Override
  final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    this.stableIntervalMicros = stableIntervalMicros;
    doSetRate(permitsPerSecond, stableIntervalMicros);
  }

其中resync方法是一個(gè)關(guān)鍵的方法粪摘,在請(qǐng)求令牌時(shí)也會(huì)用到,后面還會(huì)說(shuō)明:

 void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;
    }
  }

從中可以看出绍坝,如果nowMicros大于nextFreeTicketMicros徘意,會(huì)重新計(jì)算nextFreeTicketMicros和storedPermit的值。設(shè)置stableIntervalMicros轩褐,該字段表示1/QPS椎咧,即生產(chǎn)令牌的速率。
接著調(diào)用doSetRate方法把介,該方法在SmoothBursty類(lèi)中勤讽。

@Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        storedPermits =
          (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
}

初始化maxPermits和storePermits,后者永遠(yuǎn)不會(huì)大于前者拗踢。
到此脚牍,rateLimiter初始化完成。除了resync方法巢墅,在不重新設(shè)置rate的情況诸狭,其他方法不在處理請(qǐng)求時(shí)用到,暫時(shí)忽略砂缩。
下面看關(guān)鍵的令牌申請(qǐng)的過(guò)程作谚。
首先調(diào)用acquire()方法,申請(qǐng)令牌庵芭,無(wú)參數(shù)表示申請(qǐng)一個(gè)妹懒。

public double acquire() {
    return acquire(1);
  }

接著調(diào)用acquire(int permits)方法:

@CanIgnoreReturnValue
  public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

reserve方法返回獲取令牌所需要等待的時(shí)間,stopwatch阻塞當(dāng)前線程双吆,最后返回線程休眠的秒數(shù)眨唬。如果microsToWait為0会前,表示立即返回。

final long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

reserve需要獲取鎖才可以操作匾竿,這也是令牌桶線程安全的原因瓦宜,以下操作都在同步代碼塊中。
繼續(xù)reserveAndGetWaitLength方法岭妖。

final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }

首先調(diào)用reserveEarliestAvailable临庇,方法名說(shuō)明了返回值的意義:即返回滿足當(dāng)前請(qǐng)求的最早的時(shí)鐘,該值大于等于nowMicros昵慌。如何保證這一點(diǎn)的呢假夺?我們看該方法:

@Override
  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

這十多行代碼是整個(gè)算法實(shí)現(xiàn)的核心,重點(diǎn)說(shuō)明:

  1. 首先調(diào)用resync(nowMicros)斋攀,重置nextFreeTicketMicros已卷。如果nowMicros在nextFreeTicketMicros之后,nextFreeTicketMicros=nowMicros淳蔼,并往storedPermits中增加這段時(shí)間能產(chǎn)生的令牌侧蘸。
    返回值設(shè)置為當(dāng)前的nextFreeTicketMicros。為什么要這樣設(shè)置呢鹉梨?因?yàn)槿绻鹡owMicros大于nextFreeTicketMicros讳癌,說(shuō)明令牌桶肯定能滿足需求(無(wú)論請(qǐng)求的令牌數(shù)目是多少,參見(jiàn)最上面的設(shè)計(jì)哲學(xué))俯画,而resync方法已經(jīng)修改了nextFreeTicketMicros值為nowMicros值析桥,逐層返回給調(diào)用者時(shí),等待時(shí)間為0艰垂,線程無(wú)需等待;反之埋虹,如果nowMicros小于等于nextFreeTicketMicros猜憎,說(shuō)明請(qǐng)求過(guò)快,線程需要等待搔课,等待的時(shí)間就是nextFreeTicketMicros-nowMicros胰柑。
  2. 接下來(lái),storedPermitsToSpend代表令牌桶中已有的令牌數(shù)爬泥,可以用于當(dāng)前的請(qǐng)求。但未必滿足需求袍啡。
  3. 其次,freshPermits代表需要新生成的令牌數(shù)蔗牡。如果storedPermits已經(jīng)滿足需求颖系,則freshPermits為0。
  4. 再次辩越,計(jì)算新生成令牌需要花費(fèi)的時(shí)間,這些需要后來(lái)者償還黔攒。
  5. 然后修改nextFreeTicketMicros的值。
  6. 最后修改storedPermits值督惰。
    至此整個(gè)處理過(guò)程結(jié)束莲绰。

經(jīng)過(guò)上面的代碼梳理,詳細(xì)大家對(duì)RateLimiter的代碼有個(gè)比較清晰的認(rèn)識(shí)姑丑,但要加深理解蛤签,還需要多做debug和test震肮。
Guava包里面包括了很多test case留拾。我們可以把test類(lèi)單拿出來(lái),根據(jù)自己的情況添加相應(yīng)的case即可痴柔。該類(lèi)是com.google.common.util.concurrent. RateLimiterTest。由于很多類(lèi)都使用了默認(rèn)訪問(wèn)權(quán)限咳蔚,我們需要定義一個(gè) com.google.common.util.concurrent包谈火,導(dǎo)入RateLimiterTest類(lèi)。該類(lèi)中糯耍,guava提供了一個(gè)FakeStopwatch的nested class。它能夠讓時(shí)鐘按照我們的要求暫停革为,休眠隨意的時(shí)長(zhǎng)舵鳞,并記錄休眠和請(qǐng)求對(duì)應(yīng)的事件,并已特定的格式輸出恳蹲。例如:R1.00代表請(qǐng)求給定的令牌延遲了1秒;U1.05表示stopwatch休眠1.05秒嘉蕾,即模擬時(shí)鐘過(guò)了1.05秒。例如一個(gè)測(cè)試通過(guò)的case:

    public void testSimple() {
        RateLimiter limiter = RateLimiter.create(5.0, stopwatch);
        limiter.acquire(); // R0.00
        limiter.acquire(); // R0.20
        limiter.acquire(); // R0.20
        stopwatch.sleepMillis(1000); // U1.00
        assertEvents("R0.00", "R0.20", "R0.20", "U1.00");
    }

下面提供一個(gè)case儡率,驗(yàn)證下大家的理解以清。

public void testOneSecondBurst3() {
        RateLimiter limiter = RateLimiter.create(1.0, stopwatch);
        limiter.acquire(1); // R值?
        stopwatch.sleepMillis(1050);//U值眉孩?
        limiter.acquire(1); // R值勒葱? nowMicros? nextFree凛虽?
        stopwatch.sleepMillis(950);
        limiter.acquire(1); // R值凯旋? nowMicros? nextFree至非?
        stopwatch.sleepMillis(1000);
        limiter.acquire(1); // R值? nowMicros踏幻? nextFree戳杀?
}

關(guān)注公眾號(hào)“碼農(nóng)走向藝術(shù)”夭苗,回復(fù)消息可以獲取答案幺:)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末题造,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子界赔,更是在濱河造成了極大的恐慌,老刑警劉巖咐低,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件见擦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鲤屡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)卢未,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人堰汉,你說(shuō)我怎么就攤上這事〉” “怎么了矮固?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)档址。 經(jīng)常有香客問(wèn)我,道長(zhǎng)绎秒,這世上最難降的妖魔是什么尼摹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮玄呛,結(jié)果婚禮上和二,老公的妹妹穿的比我還像新娘。我一直安慰自己惕它,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布郁惜。 她就那樣靜靜地躺著揭北,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搔体。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音呆奕,去河邊找鬼。 笑死绳泉,一個(gè)胖子當(dāng)著我的面吹牛姆泻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播四苇,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼方咆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了榆骚?” 一聲冷哼從身側(cè)響起煌集,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體放钦,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡操禀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年横腿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揪惦。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罗侯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纫塌,到底是詐尸還是另有隱情讲弄,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布怎披,位于F島的核電站驹饺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赏壹。R本人自食惡果不足惜蝌借,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望自晰。 院中可真熱鬧稍坯,春花似錦搓劫、人聲如沸混巧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)傍衡。三九已至,卻和暖如春蛙埂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滔迈。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工被辑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盼理。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像奏路,于是被迫代替她去往敵國(guó)和親臊诊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容