常用限流方案的設計和實現

爲了保證在業務高峯期,線上系統也能保證一定的彈性和穩定性,最有效的方案就是進行服務降級了,而限流就是降級系統最常採用的方案之一。

 

限流即流量限制,或者高大上一點,叫做流量整形,限流的目的是在遇到流量高峯期或者流量突增(流量尖刺)時,把流量速率限制在系統所能接受的合理範圍之內,不至於讓系統被高流量擊垮。

 

其實,服務降級系統中的限流並沒有我們想象的那麼簡單,第一,限流方案必須是可選擇的,沒有任何方案可以適用所有場景,每種限流方案都有自己適合的場景,我們得根據業務和資源的特性和要求來選擇限流方案;第二,限流策略必須是可配的,對策略調優一定是個長期的過程,這裏說的策略,可以理解成建立在某個限流方案上的一套相關參數。

 

目前有幾種常見的限流方式:

  1. 通過限制單位時間段內調用量來限流
  2. 通過限制系統的併發調用程度來限流
  3. 使用漏桶(Leaky Bucket)算法來進行限流
  4. 使用令牌桶(Token Bucket)算法來進行限流

 

下面我們來一起討論各種限流方式的設計和具體使用場景。

 

對於第1種,通過限制某個服務的單位時間內的調用量來進行限流。從字面上,確實很容易理解,我們需要做的就是通過一個計數器統計單位時間段某個服務的訪問量,如果超過了我們設定的閾值,則該單位時間段內則不允許繼續訪問、或者把接下來的請求放入隊列中等待到下一個單位時間段繼續訪問。這裏,計數器在需要在進入下一個單位時間段時先清零。

我們來看看在Java語言中,這種方式具體應該如何做,第一步我們需要做的就是確定這個單位時間段有多長,肯定不能太長,太長將會導致限流的效果變得不夠“敏感”,因爲我們知道,進入限流階段後,如果採用的手段是不允許繼續訪問,那麼在該單位時間段內,該服務是不可用的,比如我們把單位時間設置成1小時,如果在第29分鐘,該服務的訪問量就達到了我們設定的閾值,那麼在接下來的31分鐘,該服務都將變得不可用,這無形SO BAD!!如果單位時間段設置得太短,越短的單位時間段將導致我們的閾值越難設置,比如1秒鐘,因爲高峯期的1秒鐘和低峯期的1秒鐘的流量有可能相差百倍甚至千倍,同時過短的單位時間段也對限流代碼片段提出了更高要求,限流部分的代碼必須相當穩定並且高效!最優的單位時間片段應該以閾值設置的難易程度爲標準,比如我們的監控系統統計的是服務每分鐘的調用量,所以很自然我們可以選擇1分鐘作爲時間片段,因爲我們很容易評估每個服務在高峯期和低峯期的分鐘調用量,並可以通過服務在每分鐘的平均耗時和異常量來評估服務在不同單位時間段的服務質量,這給閾值的設置提供了很好的參考依據。當單位時間段和閾值已經確定,接下來就該考慮計數器的實現了,最快能想到的就是AtomicLong了,對於每次服務調用,我們可以通過AtomicLong#incrementAndGet()方法來給計數器加1並返回最新值,我們可以通過這個最新值和閾值來進行比較來看該服務單位時間段內是否超過了閾值。這裏,如何設計計數器是個關鍵,假設單位時間段爲1分鐘,我們可以做一個環狀結構的計數器,如下:



 

當然我們可以直接用一個數組來實現它:new AtomicLong[]{new AtomicLong(0), new AtomicLong(0), newAtomicLong(0)},當前分鐘AtomicLong保存的是當前單位時間段內該服務的調用次數,上一分鐘顯然保存的是上一單位時間段的統計數據,之所以有這個是爲了統計需要,既然到了當前的單位時間段,說明上一分鐘的訪問統計已經結束,即可將上一分鐘的該接口的訪問量數據打印日誌或發送到某個服務端進行統計,因爲我們說過,閾值的設置是個不斷調優的過程,所以有時候這些統計數據會很有用。在對當前時間段的訪問量進行統計的時候,需要將下一分鐘的AtomicLong清零,這一步是很關鍵的,有兩種清零方案:第一種,直接(通過Executors.newSingleThreadScheduledExecutor)起個單獨線程,比如每隔50秒(這個當然必須小於單位時間段)對下一分鐘的AtomicLong進行清零。第二種,每次在給當前分鐘AtomicLong1時,對下一分鐘的AtomicLong的值進行檢測,如果不是0,則設置成0,如果採用這種方案,這裏會有一個bug,如果某個服務在某個完整的單位時間段內一次也沒有被調用,則下一分鐘的AtomicLong在使用前顯然沒有被清0,所以採用第二種方案還得通過額外的一個字段保存上一次清0的時間,每次使用當前分鐘AtomicLong時,需要先判斷這個字段,如果超過一個單位時間段,這則需要先清0再使用。兩種方案對比來看,第一種方案實現起來更簡單。對於如何訪問當前分鐘、上一分鐘和下一分鐘的AtomicLong,可以直接通過當前分鐘數來對數組的length取模即可(比如獲取當前分鐘的數據index(System.currentTimeMillis() / 60000) % 3)。

對於限制單位時間段內調用量的這種限流方式,實現簡單,適用於大多數場景,如果閾值可以通過服務端來動態配置,甚至可以當做業務開關來使用,但也有一定的侷限性,因爲我們的閾值是通過分析單位時間段內調用量來設置的,如果它在單位時間段的前幾秒就被流量突刺消耗完了,將導致該時間段內剩餘的時間內該服務“拒絕服務”,可以將這種現象稱爲“突刺消耗”,但慶幸的是,這種情況並不常見。

 

2種說的是通過併發限制來限流,我們通過嚴格限制某服務的併發訪問程度,其實也就限制了該服務單位時間段內的訪問量,比如限制服務的併發訪問數是100,而服務處理的平均耗時是10毫秒,那麼1分鐘內,該服務平均能提供( 1000 / 10 ) * 60 * 100 = 6,000 次,而且這個於第1種限流方案相比,它有着更嚴格的限制邊界,因爲如果採用第1種限流方案,如果大量服務調用在極短的時間內產生,仍可能壓垮系統,甚至產生雪崩效應。但是併發的閾值設置成多少比較合適呢?大多數業務的監控系統不會去統計某個服務在單位時間段內的併發量,這是因爲很多監控系統都是通過業務日誌來做到“異步”調用量的統計,但如果要統計併發量,則需要嵌入到代碼調用層面中去,比如通過AOP,如果要這樣的話,監控系統最好能和RPC框架或者服務治理等框架來配合使用,這樣對於開發人員來說,才足夠透明。介於上面所說的,我們很難去評估某個服務在業務高峯期和低峯期的併發量,這給併發閾值的設置帶來了困難,但我們可以通過線上業務的監控數據來逐步對併發閾值進行調優,只要肯花時間,我們總能找到一個即能保證一定服務質量又能保證一定服務吞吐量的合理併發閾值。用Java來實現併發也相當簡單,我們第一個想到的可能就是信號量Semaphore,在服務的入口調用非阻塞方法Semaphore#tryAcquire()來嘗試獲取一個信號量,如果失敗,則直接返回或將調用放入某個隊列中,然後在finally語句塊中調用Semaphore#release(),這裏需要注意,如果某個調用請求沒有獲取信號量成功,那麼它不應該調用release方法來釋放信號量(具體原因分析可以參考:http://manzhizhen.iteye.com/blog/2307298)。我們常常會希望還有如下效果:

  1. 既想控制某服務的併發訪問程度,又想知道實際的最高併發量會達到多少。
  2. 能動態調整併發量數據。

所以,在具體實現時,你得格外小心,比如Semaphore沒有提供重設信號量的方法(原因參看:http://manzhizhen.iteye.com/blog/2307298),所以在需要修改信號量數量時,只能重新new一個Semaphore但這是在Semaphore使用中過程中進行修改的,需要確保tryAcquirerelease操作是在同一個Semaphore之上。

總所周知,併發量限流一般用於對於服務資源有嚴格限制的場景,比如連接數、線程數等,但也未嘗不能用於通用的服務場景。現在業務的複雜性,給系統的設計帶來了一定挑戰,而且隨着業務的發展,系統的架構會不斷的拆分並服務化,下面的服務依賴場景會很常見:



  

從上圖可以看出上游的AB服務直接依賴了下游的基礎服務CDE,對於AB服務都依賴的基礎服務D這種場景,服務AB其實處於某種競爭關係,當我們考量服務A的併發閾值時,不可忽略的是服務BA的影響,所以,大多情況併發閾值的設置需要保守一點,如果服務A的併發閾值設置過大,當流量高峯期來臨,有可能直接拖垮基礎服務D並影響服務B,即雪崩效應來了。從表面上看併發量限流似乎很有用,但也不可否認,它仍然可以造成流量尖刺,即每臺服務器上該服務的併發量從0上升到閾值是沒有任何“阻力”的,這是因爲併發量考慮的只是服務能力邊界的問題。

 

3種是通過漏桶算法來進行限流,漏桶算法是網絡中流量整形的常用算法之一,它有點像我們生活中用到的漏斗,液體倒進去以後,總是從下端的小口中以固定速率流出,漏桶算法也類似,不管突然流量有多大,漏桶都保證了流量的常速率輸出,也可以類比於調用量,比如,不管服務調用多麼不穩定,我們只固定進行服務輸出,比如每10毫秒接受一次服務調用。既然是一個桶,那就肯定有容量,由於調用的消費速率已經固定,那麼當桶的容量堆滿了,則只能丟棄了,漏桶算法如下圖:



 

 

漏桶算法其實是悲觀的,因爲它嚴格限制了系統的吞吐量,從某種角度上來說,它的效果和併發量限流很類似。漏桶算法也可以用於大多數場景,但由於它對服務吞吐量有着嚴格固定的限制,如果在某個大的服務網絡中只對某些服務進行漏桶算法限流,這些服務可能會成爲瓶頸。其實對於可擴展的大型服務網絡,上游的服務壓力可以經過多重下游服務進行擴散,過多的漏桶限流似乎意義不大。

實現方面,可以先準備一個隊列,當做桶的容量,另外通過一個計劃線程池(ScheduledExecutorService)來定期從隊列中獲取並執行請求調用,當然,我們沒有限定一次性只能從隊裏中拿取一個請求,比如可以一次性拿100個請求,然後併發執行。

 

4種是令牌桶算法限流,令牌桶算法從某種程度上來說是漏桶算法的一種改進,漏桶算法能夠強行限制請求調用的速率,而令牌桶算法能夠在限制調用的平均速率的同時還允許某種程度的突發調用。在令牌桶算法中,桶中會有一定數量的令牌,每次請求調用需要去桶中拿取一個令牌,拿到令牌後纔有資格執行請求調用,否則只能等待能拿到足夠的令牌數,讀者看到這裏,可能就認爲是不是可以把令牌比喻成信號量,那和前面說的併發量限流不是沒什麼區別嘛?其實不然,令牌桶算法的精髓就在於“拿令牌”和“放令牌”的方式,這和單純的併發量限流有明顯區別,採用併發量限流時,當一個調用到來時,會先獲取一個信號量,當調用結束時,會釋放一個信號量,但令牌桶算法不同,因爲每次請求獲取的令牌數不是固定的,比如當桶中的令牌數還比較多時,每次調用只需要獲取一個令牌,隨着桶中的令牌數逐漸減少,當到令牌的使用率(即使用中的令牌數/令牌總數)達某個比例,可能一次請求需要獲取兩個令牌,當令牌使用率到了一個更高的比例,可能一次請求調用需要獲取更多的令牌數。同時,當調用使用完令牌後,有兩種令牌生成方法,第一種就是直接往桶中放回使用的令牌數,第二種就是不做任何操作,有另一個額外的令牌生成步驟來將令牌勻速放回桶中。如下圖:



 

 

在併發量限制時,在達到併發閾值之前,併發量和前來調用的線程數可以說是成嚴格正比關係的,但在令牌桶中可能就並不是這樣,下面給出在某種特定場景和特定參數下四種限流方式對服務併發能力影響的折線圖,其中X軸表示當前併發調用數,而Y軸表示某服務在不同併發調用程度下采取限流後的實際併發調用數:

 

 

 

其實,不同場景不同參數下,服務採用所述四種限流方式都會有不同的效果,甚至較大差異,上圖也並不能說明實際併發度越高就吞吐量越高,因爲還必須把穩定性等因素考慮進去,這就好比插入排序、堆排序、歸併排序和快速排序的對比一樣,沒有任何限流算法可以說自己在任何場景下都是最優限流算法,這需要從服務資源特性、限流策略(參數)配置難度、開發難度和效果檢測難度等多方面因素來考慮。

 

咱們再回到令牌桶算法,和其他三種降級方式來說,令牌桶算法限流無疑是最靈活的,因爲它有衆多可配置的參數來直接影響限流的效果。幸運的是,谷歌的Guava包中RateLimiter提供了令牌桶算法的實現.

 

我們先看看如何創建一個RateLimiter實例:

RateLimiter create(double permitsPerSecond);  // 創建一個每秒包含permitsPerSecond個令牌的令牌桶,可以理解爲QPS最多爲permitsPerSecond

RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)// 創建一個每秒包含permitsPerSecond個令牌的令牌桶,可以理解爲QPS最多爲permitsPerSecond,幷包含某個時間段的預熱期

 

我們再看看獲取令牌的相關方法:

double acquire(); // 阻塞直到獲取一個許可,返回被限制的睡眠等待時間,單位秒

double acquire(int permits); // 阻塞直到獲取permits個許可,返回被限制的睡眠等待時間,單位秒

boolean tryAcquire();  // 嘗試獲取一個許可

boolean tryAcquire(int permits);  // 嘗試獲取permits個許可

boolean tryAcquire(long timeout, TimeUnit unit);  // 嘗試獲取一個許可,最多等待timeout時間

boolean tryAcquire(int permits, long timeout, TimeUnit unit);  // 嘗試獲取permits個許可,最多等待timeout時間

 

我們來看個最簡單的例子:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss.SSS");

RateLimiter rateLimiter = RateLimiter.create(2);

while(true) {

    rateLimiter.acquire();

    System.out.println(simpleDateFormat.format(new Date()));

}

運行該例子會得到類似如下結果:

20160716 17:04:03.352

20160716 17:04:03.851

20160716 17:04:04.350

20160716 17:04:04.849

20160716 17:04:05.350

20160716 17:04:05.850

20160716 17:04:06.350

20160716 17:04:06.850

 

其實,單從RateLimiter實例的創建方法來說,它更像是漏桶算法的實現(前面說的第3種方式),或者像一個單位時間段爲1秒的調用量限流方式(前面說的第1種方式),唯一看起來像令牌桶算法的是其獲取信號量的時候,可以一次性嘗試獲取多個令牌。就像上面的例子,我們通過RateLimiter.create(2)創建了一個每秒會產生兩個令牌的令牌桶,也就是說每秒你最多能獲取兩個令牌,如果每次調用都需要獲取一個令牌,那麼就限制了QPS最多爲2RateLimiter對外沒有提供釋放令牌的release方法,而是默認會每秒往桶中放入兩個令牌。所以,在接下來的while循環中,我們獲取一個令牌後直接打印當前的時間,可以看出,每秒將打印兩次。從例子可以看出,我們故意打出了毫秒數,可以看出,令牌的獲取也是有固定間隔的,因爲我們是每秒兩個令牌,所以每個令牌的獲取間隔大約是500毫秒。

 

RateLimiter提供的操作來看,要實現一個實用的漏桶算法限流工具還有些路要走,比如

  1. 它似乎不允許併發,雖然當我們每秒設置的令牌數足夠多時或者服務處理時間超過1秒時,效果和併發類似。
  2. 它沒有提供一些通用的函數,來表式令牌使用率和獲取令牌數之間的關係,需要外部實現。
  3. 除了默認的自動添加令牌的方式,如果能提供手動釋放令牌的方式,適用的的場景可能會更多。

 

撇開這些不說,我們還是來看看其中的具體實現吧。抽象類RateLimitercreate方法返回的是一個“平滑突發”類型SmoothBursty的實例:

public static RateLimiter create(double permitsPerSecond) {

  return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond);

}

@VisibleForTesting

static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {

  // 這裏就默認設置了1秒鐘,表示如果SmoothBursty沒被使用,許可數能被保存1秒鐘

  RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);

  rateLimiter.setRate(permitsPerSecond);

  return rateLimiter;

}

 

SmoothBursty的構造函數有兩個入參,一個是SleepingStopwatch 實例,它是一個阻止睡眠觀察器,可以理解成一個鬧鐘,如果你想睡十秒,它會在10秒鐘內一直阻塞你(讓你睡覺),到了十秒鐘,它將對你放行(喚醒你),另一個參數是許可被保存的秒數,因爲RateLimiter的行爲是每過一秒會自動補充令牌,所以前一秒的令牌需要“被過期”。我們直接看獲取令牌的阻塞實現acquire

@CanIgnoreReturnValue

public double acquire(int permits) {

  // 預定permits個許可供未來使用,並返回等待這些許可可用的微秒數

  long microsToWait = reserve(permits);

  // 等待指定的時間

  stopwatch.sleepMicrosUninterruptibly(microsToWait);

  // 返回消耗的時間

  return 1.0 * microsToWait / SECONDS.toMicros(1L);

}

 

可以看出步驟很少,就是先預定,然後等待,最後返回,我們先來看預定步驟的實現:

final long reserve(int permits) {

  // permits的值必須大於0

  checkPermits(permits);

  // 這裏通過一個對象鎖進行同步

  synchronized (mutex()) {

    // 預定permits數目的許可,並返回調用方需要等待的時間

    return reserveAndGetWaitLength(permits, stopwatch.readMicros());

  }

}

// Can't be initialized in the constructor because mocks don't call the constructor.

private volatile Object mutexDoNotUseDirectly;

private Object mutex() {

  Object mutex = mutexDoNotUseDirectly;

  if (mutex == null) {

    synchronized (this) {

      mutex = mutexDoNotUseDirectly;

      if (mutex == null) {

        mutexDoNotUseDirectly = mutex = new Object();

      }

    }

  }

  return mutex;

}

 

當拿到mutexDoNotUseDirectly鎖後,我們看看reserveAndGetWaitLength的實現:

final long reserveAndGetWaitLength(int permits, long nowMicros) {

  // reserveEarliestAvailableRateLimiter的子類SmoothRateLimiter來實現

  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);

  return max(momentAvailable - nowMicros, 0);

}

 

可以看出,獲取許可需要等待的時間是由reserveEarliestAvailable方法來實現的,它是RateLimiter的抽象方法,由其抽象子類SmoothRateLimiter來實現,在看reserveEarliestAvailable方法的實現之前,我們先來看看SmoothRateLimiter類中的主要屬性:

// 最大許可數,比如RateLimiter.create(2);表明最大的許可數就是2

double maxPermits;

// 當前剩餘的許可數

double storedPermits;

// 固定的微秒週期

double stableIntervalMicros;

// 下一個空閒票據的時間點

private long nextFreeTicketMicros 0L;

因爲RateLimiter需要保證許可被穩定連續的輸出,比如每秒有5個許可,那麼你獲取許可的間隔時間是200毫秒,而stableIntervalMicros就是用來保存這個固定的間隔時間的,方便後面計算使用。

SmoothRateLimiter有兩個子類:SmoothBurstySmoothWarmingUp,從名字可以看出,SmoothWarmingUp類的實現帶有預熱功能,而SmoothBursty類是沒有的,什麼是預熱功能呢?就好比緩存一樣,當使用SmoothWarmingUp的實現時,不會在前幾秒就給足全量的許可,就是說許可數會慢慢的增長,知道達到我們預定義的值,所以,SmoothRateLimiter會留下如下抽象方法交給其子類來實現:

/重設流量相關參數,需要子類來實現,不同子類參數不盡相同,比如SmoothWarmingUp肯定有增長比率相關參數

void doSetRate(double permitsPerSecond, double stableIntervalMicros);

/計算生成這些許可數需要等待的時間

long storedPermitsToWaitTime(double storedPermits, double permitsToTake);

/返回許可冷卻(間隔)時間

double coolDownIntervalMicros();

 

 

我們來看看SmoothRateLimiter中該方法的實現:

@Override

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {

  // 基於當前時間nowMicros來更新storedPermitsnextFreeTicketMicros

  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;

}

從上面的代碼片段可以看出,該方法先會去調用resync方法來更新當前可用許可數(nowMicros)和下一個空閒票據的時間(nextFreeTicketMicros )。我們看下resync的實現:

void resync(long nowMicros) {

  // 如果下一空閒票據時間已經小於當前時間,說明需要更新當前數據

  if (nowMicros > nextFreeTicketMicros) {

    // 根據當前時間和上一次獲取許可時間的間隔時間,來計算應該有多少許可被生成

    double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();

    // 更新當前可用許可

    storedPermits min(maxPermitsstoredPermits + newPermits);

    nextFreeTicketMicros = nowMicros;

  }

}

從這裏我們可以看出,補充當前可用許可是在每次獲取許可數步驟中的第一步完成的。

reserveEarliestAvailable方法中,storedPermitsToWaitTime方法的調用是其關鍵步驟,前面說過,它由子類實現,子類SmoothBursty類的storedPermitsToWaitTime實現相當簡單:

long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

    return 0L;

}

所以,在這裏,waitMicros的值是:0 + (long) (freshPermits * stableIntervalMicros),即還差的許可數乘以許可間隔時間,很自然在這裏下一次能獲取許可的時間(nextFreeTicketMicros )是nextFreeTicketMicros + freshPermits * stableIntervalMicros從這裏可以得出,假設我們配置的是每秒5個許可,當我們在第0毫秒時獲取第一個許可可以馬上拿到,當我們在第100毫秒想要再次獲取1個許可,還需要等100毫秒,當時間到了210毫秒時,如果我們這是想要獲取兩個許可,則需要等待200 * 2 + 200 - 210 = 390毫秒(這裏加的200毫秒是上一次獲取許可的時間).

 

現在我們回過頭來看acquire方法,當拿到下一個許可的等待時間後,將調用SleepingStopwatch#sleepMicrosUninterruptibly來阻塞等待,先說說Stopwatch類,Stopwatch用來測量時間,可以理解成guava包中使用System#nanoTime的替代品,SleepingStopwatch也使用委派方式來使用Stopwatch的計算時間的功能,但sleepMicrosUninterruptiblySleepingStopwatch的內部實現:

@Override

protected void sleepMicrosUninterruptibly(long micros) {

  if (micros > 0) {

    Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS);

  }

}

可見,阻塞任務是由Uninterruptibles來完成的,Uninterruptiblesguava中的阻塞工具類,我們直接看它的sleepUninterruptibly方法的實現:

@GwtIncompatible // concurrency

public static void sleepUninterruptibly(long sleepFor, TimeUnit unit) {

  boolean interrupted = false;

  try {

    long remainingNanos = unit.toNanos(sleepFor);

    long end = System.nanoTime() + remainingNanos;

    while (true) {

      try {

        // TimeUnit.sleep() treats negative timeouts just like zero.

        NANOSECONDS.sleep(remainingNanos);

        return;

      } catch (InterruptedException e) {

        interrupted = true;

        remainingNanos = end - System.nanoTime();

      }

    }

  } finally {

    if (interrupted) {

      Thread.currentThread().interrupt();

    }

  }

}

可見,它採用的是TimeUnit#sleep方法。

 

到這裏,RateLimiter#acquire獲取許可的操作就介紹完畢了,可見,其中並沒有複雜的算法或技巧,RateLimiter還提供了重設許可數的方法:

void doSetRate(double permitsPerSecond, long nowMicros);

 

 

就像本文提到的,使用令牌桶時,桶中的令牌數量和每次獲取的令牌數是個可調的參數,如果按照RateLimiter這種設計的話,多個令牌數的獲取將要等待多個間隔時間,這也許不是我們想要的。

  • 大小: 19.8 KB
  • 大小: 12.7 KB
  • 大小: 30.2 KB
  • 大小: 9.7 KB
  • 大小: 31 KB
版權聲明:本文爲博主manzhizhen的原創文章,未經博主允許不得轉載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章