計(jì)數(shù)器算法
計(jì)數(shù)器算法是限流算法中最簡(jiǎn)單的一種算法,限制在一個(gè)時(shí)間窗口內(nèi),至多處理多少個(gè)請(qǐng)求。比如每分鐘最多處理10個(gè)請(qǐng)求,則從第一個(gè)請(qǐng)求進(jìn)來(lái)的時(shí)間為起點(diǎn),60s的時(shí)間窗口內(nèi)只允許最多處理10個(gè)請(qǐng)求。下一個(gè)時(shí)間窗口又以前一時(shí)間窗口過(guò)后第一個(gè)請(qǐng)求進(jìn)來(lái)的時(shí)間為起點(diǎn)。常見(jiàn)的比如一分鐘內(nèi)只能獲取一次短信驗(yàn)證碼的功能可以通過(guò)計(jì)數(shù)器算法來(lái)實(shí)現(xiàn)。
Guava RateLimiter解析
Guava是Google開(kāi)源的一個(gè)工具包,其中的RateLimiter是實(shí)現(xiàn)了令牌桶算法的一個(gè)限流工具類(lèi)。在pom.xml中添加guava依賴(lài),即可使用RateLimiter
<dependency>
? ? <groupId>com.google.guava</groupId>
? ? <artifactId>guava</artifactId>
? ? <version>29.0-jre</version>
</dependency>
如下測(cè)試代碼示例了RateLimiter的用法,
public static void main(String[] args) {
? ? RateLimiter rateLimiter = RateLimiter.create(1); //創(chuàng)建一個(gè)每秒產(chǎn)生一個(gè)令牌的令牌桶
? ? for(int i=1;i<=5;i++) {
? ? ? ? double waitTime = rateLimiter.acquire(i); //一次獲取i個(gè)令牌
? ? ? ? System.out.println("acquire:" + i + " waitTime:" + waitTime);
? ? }
}
運(yùn)行后,輸出如下,
acquire:1 waitTime:0.0
acquire:2 waitTime:0.997729
acquire:3 waitTime:1.998076
acquire:4 waitTime:3.000303
acquire:5 waitTime:4.000223
第一次獲取一個(gè)令牌時(shí),等待0s立即可獲取到(這里之所以不需要等待是因?yàn)榱钆仆暗念A(yù)消費(fèi)特性),第二次獲取兩個(gè)令牌,等待時(shí)間1s,這個(gè)1s就是前面獲取一個(gè)令牌時(shí)因?yàn)轭A(yù)消費(fèi)沒(méi)有等待延到這次來(lái)等待的時(shí)間,這次獲取兩個(gè)又是預(yù)消費(fèi),所以下一次獲取(取3個(gè)時(shí))就要等待這次預(yù)消費(fèi)需要的2s了,依此類(lèi)推。可見(jiàn)預(yù)消費(fèi)不需要等待的時(shí)間都由下一次來(lái)買(mǎi)單,以保障一定的平均處理速率(上例為1s一次)。
RateLimiter有兩種實(shí)現(xiàn):
SmoothBursty: 令牌的生成速度恒定。使用 RateLimiter.create(double permitsPerSecond) 創(chuàng)建的是 SmoothBursty 實(shí)例。
SmoothWarmingUp:令牌的生成速度持續(xù)提升,直到達(dá)到一個(gè)穩(wěn)定的值。WarmingUp,顧名思義就是有一個(gè)熱身的過(guò)程。使用 RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 時(shí)創(chuàng)建就是 SmoothWarmingUp 實(shí)例,其中 warmupPeriod 就是熱身達(dá)到穩(wěn)定速度的時(shí)間。
類(lèi)結(jié)構(gòu)如下
ratelimiter-struct
關(guān)鍵屬性及方法解析(以 SmoothBursty 為例)
1.關(guān)鍵屬性
/** 桶中當(dāng)前擁有的令牌數(shù). */
double storedPermits;
/** 桶中最多可以保存多少秒存入的令牌數(shù) */
double maxBurstSeconds;
/** 桶中能存儲(chǔ)的最大令牌數(shù),等于storedPermits*maxBurstSeconds. */
double maxPermits;
/** 放入令牌的時(shí)間間隔*/
double stableIntervalMicros;
/** 下次可獲取令牌的時(shí)間點(diǎn),可以是過(guò)去也可以是將來(lái)的時(shí)間點(diǎn)*/
private long nextFreeTicketMicros = 0L;
2.關(guān)鍵方法
調(diào)用 RateLimiter.create(double permitsPerSecond) 方法時(shí),創(chuàng)建的是 SmoothBursty 實(shí)例,默認(rèn)設(shè)置 maxBurstSeconds 為1s。SleepingStopwatch 是guava中的一個(gè)時(shí)鐘類(lèi)實(shí)現(xiàn)。
@VisibleForTesting
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
super(stopwatch);
this.maxBurstSeconds = maxBurstSeconds;
}
并通過(guò)調(diào)用 SmoothBursty.doSetRate(double, long) 方法進(jìn)行初始化,該方法中:
調(diào)用 resync(nowMicros) 對(duì) storedPermits 與 nextFreeTicketMicros 進(jìn)行了調(diào)整——如果當(dāng)前時(shí)間晚于 nextFreeTicketMicros,則計(jì)算這段時(shí)間內(nèi)產(chǎn)生的令牌數(shù),累加到 storedPermits 上,并更新下次可獲取令牌時(shí)間 nextFreeTicketMicros 為當(dāng)前時(shí)間。
計(jì)算 stableIntervalMicros 的值,1/permitsPerSecond。
調(diào)用 doSetRate(double, double) 方法計(jì)算 maxPermits 值(maxBurstSeconds*permitsPerSecond),并根據(jù)舊的 maxPermits 值對(duì) storedPermits 進(jìn)行調(diào)整。
源碼如下所示
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
/** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
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;
}
}
@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;
}
}
調(diào)用 acquire(int) 方法獲取指定數(shù)量的令牌時(shí),
調(diào)用 reserve(int) 方法,該方法最終調(diào)用 reserveEarliestAvailable(int, long) 來(lái)更新下次可取令牌時(shí)間點(diǎn)與當(dāng)前存儲(chǔ)的令牌數(shù),并返回本次可取令牌的時(shí)間點(diǎn),根據(jù)該時(shí)間點(diǎn)計(jì)算需要等待的時(shí)間
阻塞等待1中返回的等待時(shí)間
返回等待的時(shí)間(秒)
源碼如下所示
/** 獲取指定數(shù)量(permits)的令牌,阻塞直到獲取到令牌,返回等待的時(shí)間*/
@CanIgnoreReturnValue
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
/** 返回需要等待的時(shí)間*/
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
/** 針對(duì)此次需要獲取的令牌數(shù)更新下次可取令牌時(shí)間點(diǎn)與存儲(chǔ)的令牌數(shù),返回本次可取令牌的時(shí)間點(diǎn)*/
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros); // 更新當(dāng)前數(shù)據(jù)
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 本次可消費(fèi)的令牌數(shù)
double freshPermits = requiredPermits - storedPermitsToSpend; // 需要新增的令牌數(shù)
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros); // 需要等待的時(shí)間
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); // 更新下次可取令牌的時(shí)間點(diǎn)
this.storedPermits -= storedPermitsToSpend; // 更新當(dāng)前存儲(chǔ)的令牌數(shù)
return returnValue;
}
acquire(int) 方法是獲取不到令牌時(shí)一直阻塞,直到獲取到令牌,tryAcquire(int,long,TimeUnit) 方法則是在指定超時(shí)時(shí)間內(nèi)嘗試獲取令牌,如果獲取到或超時(shí)時(shí)間到則返回是否獲取成功
先判斷是否能在指定超時(shí)時(shí)間內(nèi)獲取到令牌,通過(guò) nextFreeTicketMicros <= timeoutMicros + nowMicros 是否為true來(lái)判斷,即可取令牌時(shí)間早于當(dāng)前時(shí)間加超時(shí)時(shí)間則可取(預(yù)消費(fèi)的特性),否則不可獲取。
如果不可獲取,立即返回false。
如果可獲取,則調(diào)用 reserveAndGetWaitLength(permits, nowMicros) 來(lái)更新下次可取令牌時(shí)間點(diǎn)與當(dāng)前存儲(chǔ)的令牌數(shù),返回等待時(shí)間(邏輯與前面相同),并阻塞等待相應(yīng)的時(shí)間,返回true。
源碼如下所示
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) { //判斷是否能在超時(shí)時(shí)間內(nèi)獲取指定數(shù)量的令牌
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; //只要可取時(shí)間小于當(dāng)前時(shí)間+超時(shí)時(shí)間,則可獲取(可預(yù)消費(fèi)的特性!)
}
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
以上就是 SmoothBursty 實(shí)現(xiàn)的基本處理流程。注意兩點(diǎn):
RateLimiter 通過(guò)限制后面請(qǐng)求的等待時(shí)間,來(lái)支持一定程度的突發(fā)請(qǐng)求——預(yù)消費(fèi)的特性。
RateLimiter 令牌桶的實(shí)現(xiàn)并不是起一個(gè)線(xiàn)程不斷往桶里放令牌,而是以一種延遲計(jì)算的方式(參考resync函數(shù)),在每次獲取令牌之前計(jì)算該段時(shí)間內(nèi)可以產(chǎn)生多少令牌,將產(chǎn)生的令牌加入令牌桶中并更新數(shù)據(jù)來(lái)實(shí)現(xiàn),比起一個(gè)線(xiàn)程來(lái)不斷往桶里放令牌高效得多。(想想如果需要針對(duì)每個(gè)用戶(hù)限制某個(gè)接口的訪(fǎng)問(wèn),則針對(duì)每個(gè)用戶(hù)都得創(chuàng)建一個(gè)RateLimiter,并起一個(gè)線(xiàn)程來(lái)控制令牌存放的話(huà),如果在線(xiàn)用戶(hù)數(shù)有幾十上百萬(wàn),起線(xiàn)程來(lái)控制是一件多么恐怖的事情)
深圳網(wǎng)站建設(shè)www.sz886.com