RateLimiter限流及源碼分析

什麼是Guava?

Guava工程包含了若干被Google的 Java項目廣泛依賴 的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [primitives support] 、併發庫 [concurrency libraries] 、通用註解 [common annotations] 、字符串處理 [string processing] 、I/O 等等。 所有這些工具每天都在被Google的工程師應用在產品服務中。

接下來開始正文

首先來看看關係圖,這裏我們可以看到,本文所講解的SmoothBursty繼承自SmoothRateLimiter,而SmoothRateLimiter又繼承自RateLimiter,看一下即可,記住這三者的關係
關係圖

<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0.1-jre</version>
</dependency>
如何使用RateLimiter實現限流?
/**
 * 演示用例1 
 * 涉及方法————
 * RateLimiter.create(double permitsPerSecond)創建一個SmoothBursty,並指定每秒生成令牌數
 * rateLimiter.acquire() 消費一個令牌
 */
public class RateLimiterDemo {
    public static void main(String[] args) {
        //每秒鐘生產2個令牌,也就是0.5s生成一個令牌
        RateLimiter rateLimiter = RateLimiter.create(2);
        //消費一個令牌 * 3次,將返回等待的時間
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
    }
}

運行結果如下——
0.49這個很容易理解,中間會經過代碼的執行會損失部分精度,但第一次爲什麼是0?
在這裏插入圖片描述

爲什麼第一次申請令牌時等待時間爲0(也就是不用進行等待)?

這裏先使用白話文解釋,後面將對源碼進行講解
——這是因爲每次申請令牌,會計算需要等待的時間,但該時間並非本次申請需要等待的時間,而是下一次申請需要等待的時間。

/**
 * 演示用例2
 * 涉及方法————
 * RateLimiter.create(double permitsPerSecond)創建一個SmoothBursty,並指定每秒生成令牌數
 * rateLimiter.acquire() 消費一個令牌
 * rateLimiter.acquire(int permits) 消費指定令牌數
 */
public class RateLimiterDemo {
    public static void main(String[] args) {
        //每秒鐘生產2個令牌,也就是0.5s生成一個令牌
        RateLimiter rateLimiter = RateLimiter.create(2);
        //消費10個令牌,本次消費等待0s,下一次消費將等待5s
        System.out.println(rateLimiter.acquire(10));
        //消費1個令牌,本次消費等待5s,下一次消費將等待0.5s
        System.out.println(rateLimiter.acquire());
        //消費2個令牌,本次消費等待0.5s,下一次消費將等待1s
        System.out.println(rateLimiter.acquire(2));
        //消費1個令牌,本次消費等待1s,
        System.out.println(rateLimiter.acquire());
    }
}

在這裏插入圖片描述
看完這段代碼相信大家已經明白了爲什麼第一次申請令牌時等待時間爲0

SmoothBursty允許一次性消費大的令牌數,如果突然間來了很大的流量,系統可能扛不住這種突發,可以避免消費大的令牌數,或者使用SmoothWarmingUp,使用它會在啓動時以一個比較大的速率慢慢達到平均速率,這裏並不會對SmoothWarmingUp進行探討。

RateLimiter還提供了另一個申請令牌方法,可以無阻塞申請,設置超時申請,使用非常簡單,這裏不介紹了
在這裏插入圖片描述
在這裏插入圖片描述

下面將對RateLimiter的源碼進行探討

屬性

public abstract class RateLimiter {
	//用於獲取時間和停止線程的秒錶
	private final RateLimiter.SleepingStopwatch stopwatch;
	//用於上鎖的互斥量
	private volatile Object mutexDoNotUseDirectly; 
	...
}

先來看看如何創建一個RateLimiter

public abstract class RateLimiter {
	/**
	 * 創建RateLimiter步驟1
	 * @param permitsPerSecond 每秒生成令牌數
	 */
	public static RateLimiter create(double permitsPerSecond) {
	    return create(permitsPerSecond,
	        RateLimiter.SleepingStopwatch.createFromSystemTimer());
	}
	...
}
這裏我們先來看看第二個參數RateLimiter.SleepingStopwatch.createFromSystemTimer()

SleepingStopwatch是RateLimiter內部的一個抽象類,定義了兩個抽象方法和一個具體方法

抽象方法

  • readMicros():獲取計時器經過時間
  • sleepMicrosUninterruptibly(long var1):讓線程等待指定毫秒數

具體方法

  • createFromSystemTimer():用於創建並啓動一個計時器,重寫兩個抽象方法
abstract static class SleepingStopwatch {
    protected SleepingStopwatch() {
    }

    protected abstract long readMicros();

    protected abstract void sleepMicrosUninterruptibly(long var1);

    public static RateLimiter.SleepingStopwatch createFromSystemTimer() {
        return new RateLimiter.SleepingStopwatch() {
        	//創建一個自動倒計時的計時器
            final Stopwatch stopwatch = Stopwatch.createStarted();
			//自開始經過的時間
            protected long readMicros() {
                return this.stopwatch.elapsed(TimeUnit.MICROSECONDS);
            }
			//將線程等待指定的毫秒時間
			//執行的是TimeUnit.NANOSECONDS.sleep()
            protected void sleepMicrosUninterruptibly(long micros) {
                if (micros > 0L) {
                    Uninterruptibles.sleepUninterruptibly(micros,
                    TimeUnit.MICROSECONDS);
                }

            }
        };
    }
}

返回去看之前的create方法(下面這個),這是我們已經知道傳入了兩個參數,一個是我們指定的每秒申請請求數,另一個是SleepingStopwatch(獲取秒錶經過毫秒時間、線程睡眠指定毫秒時間)

public abstract class RateLimiter {
	/**
	 * 創建RateLimiter步驟1
	 * @param permitsPerSecond 每秒生成令牌數
	 */
	public static RateLimiter create(double permitsPerSecond) {
	    return create(permitsPerSecond,
	        RateLimiter.SleepingStopwatch.createFromSystemTimer());
	}
	...
}

通過下面這段代碼可以看到調用RateLimiter.create(double permitsPerSecond)方法最終將創建一個SmoothBursty的實例對象(上面的關係圖沒忘吧,SmoothBursty是其子類的子類)

public abstract class RateLimiter {
	/**
	 * 創建RateLimiter步驟2
	 * @param permitsPerSecond 每秒生成令牌數
	 * @param stopwatch 用於獲取經過時間和線程等待
	 */
	static RateLimiter create(double permitsPerSecond,
	                          RateLimiter.SleepingStopwatch stopwatch) {
	    //創建一個SmoothBursty對象
	    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0D);
	    rateLimiter.setRate(permitsPerSecond);
	    return rateLimiter;
	}
	...
}

在上面的new SmoothBursty(stopwatch, 1.0D)會初始化兩個參數
maxBurstSeconds = 0 該參數來自SmoothBursty,這裏不用理會是什麼意思
nextFreeTicketMicros = 0

並將stopwatch設置到RateLimiter中
這裏來看一下SmoothRateLimiter中的四個參數

/**
 * SmoothRateLimiter是RateLimiter的抽象子類
 */
abstract class SmoothRateLimiter extends RateLimiter {
    double storedPermits; 	           //剩餘令牌數
    double maxPermits;                 //最大令牌數
    double stableIntervalMicros;       //生成令牌時間間隔
    private long nextFreeTicketMicros; //下一次可獲取令牌的時間(子類初始化時設爲0)
}

繼續創建RateLimiter步驟2的下一步
由於SmoothBursty並沒有重寫setRate()方法,將調用RateLimiter的setRate()

public abstract class RateLimiter {
	/**
	 * 創建RateLimiter步驟3
	 * @param permitsPerSecond 每秒生成令牌數
	 */
	public final void setRate(double permitsPerSecond) {
		//判斷參數合法性
	    Preconditions.checkArgument(permitsPerSecond > 0.0D &&
	                        !Double.isNaN(permitsPerSecond),
	                         "rate must be positive");
	    //this.mutex()其實就是返回上面那個互斥量
	    synchronized(this.mutex()) {
	    	//將每秒生成令牌數與秒錶經過時間作爲參數傳遞
	        this.doSetRate(permitsPerSecond, this.stopwatch.readMicros());
	    }
	}
}

接下來將會調用RateLimiter中的doSetRate()

public abstract class RateLimiter {
	/**
	 * 創建RateLimiter步驟4
	 * 初始化操作,添加令牌,設置令牌生成周期
	 * @param permitsPerSecond 每秒生成令牌數
	 * @param nowMicros 秒錶經過時間
	 */
	final void doSetRate(double permitsPerSecond, long nowMicros) {
		//添加令牌,並設置下次獲取令牌時間nextFreeTicketMicros;
	    this.resync(nowMicros);
	    //stableIntervalMicros(每多少微秒生成一個令牌)
	    double stableIntervalMicros = (double)TimeUnit.SECONDS.toMicros(1L) / 
	    	permitsPerSecond;
	    this.stableIntervalMicros = stableIntervalMicros; //初始化
	    this.doSetRate(permitsPerSecond, stableIntervalMicros);
	}
}

下面具體看一下生成令牌的resync()方法

abstract class SmoothRateLimiter extends RateLimiter {
	/**
	 * 通過(當前時間秒錶經過時間 - 上一次秒錶經過時間) / 令牌生成周期 計算需添加的令牌數
	 * 同時設置nextFreeTicketMicros爲當前時間
	 */
	void resync(long nowMicros) {
		//如果秒錶經過時間大於下一次可獲取令牌的時間
	    if (nowMicros > this.nextFreeTicketMicros) {
	    	//計算新增令牌數
	    	//t1(nowMicros):本次添加令牌時秒錶經過的時間
	    	//t2(nextFreeTicketMicros):上一次添加令牌秒錶時秒錶經過的時間
	    	//n(stableIntervalMicros):令牌生成周期
	    	//本次需要添加的令牌 = (t2 - t1) / n
	        double newPermits = (double)(nowMicros - this.nextFreeTicketMicros) / 
	            this.coolDownIntervalMicros(); //該方法返回this.stableIntervalMicros
	        //更新剩餘令牌數
	        this.storedPermits = Math.min(this.maxPermits, 
	        	this.storedPermits + newPermits);
	        //設置下一次請求可以獲取令牌的起始時間爲當前秒錶經過時間
	        this.nextFreeTicketMicros = nowMicros;
	    }
	}
}

這裏可能有點不懂,nextFreeTicketMicros關係到下一次申請需要等待的時間
RateLimiter的doSetRate(double, long)將繼續調用子類的doSetRate(double, double)

final double maxBurstSeconds; //最大突發秒數,初始化時默認爲1
/**
 * 初始化操作,設置最大令牌數和剩餘令牌數
 * @param permitsPerSecond 每秒生成令牌數
 * @param stableIntervalMicros 令牌生成周期
 */
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
	//this.maxPermits默認爲0,所以舊最大令牌數爲0
    double oldMaxPermits = this.maxPermits; 
    //如果每秒生成100個令牌,this.maxPermits爲100
    this.maxPermits = this.maxBurstSeconds * permitsPerSecond;
    //當舊最大令牌數爲無窮大,設置剩餘令牌數也爲無窮大
    //否則,由於舊最大令牌數爲0,設置剩餘令牌數也爲0
    if (oldMaxPermits == 1.0D / 0.0) {
        this.storedPermits = this.maxPermits;
    } else {
        this.storedPermits = oldMaxPermits ==
        	 0.0D ? 0.0D : this.storedPermits * this.maxPermits / oldMaxPermits;
    }

}

接下來看看消費令牌的一系列相關源碼,也可以先看最後面的總結再看源碼,或者不看

/**
 * 消費1個令牌
 */
@CanIgnoreReturnValue
public double acquire() {
    return this.acquire(1);
}

/**
 * 消費多個令牌
 */
@CanIgnoreReturnValue
public double acquire(int permits) {
	//獲取還需等待的時間
    long microsToWait = this.reserve(permits);
    //讓線程等待
    this.stopwatch.sleepMicrosUninterruptibly(microsToWait);
    //返回等待時間
    return 1.0D * (double)microsToWait / (double)TimeUnit.SECONDS.toMicros(1L);
}
/**
 * @param permits 申請令牌數
 * @return 如果未到下一次申請令牌時間將返回還需等待的時間,否則返回0
 */
final long reserve(int permits) {
    checkPermits(permits);
    synchronized(this.mutex()) {
        return this.reserveAndGetWaitLength(permits, this.stopwatch.readMicros());
    }
}
/**
 * @param permits 申請令牌數
 * @param nowMicros 秒錶經過時間
 * @return 如果未到下一次申請令牌時間將返回還需等待的時間,否則返回0
 */
final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = this.reserveEarliestAvailable(permits, nowMicros);
    return Math.max(momentAvailable - nowMicros, 0L);
}
/**
 * 1.判斷是否已到下一次添加令牌時間,是的話添加令牌,並設置下一次添加令牌時間爲當前秒錶經過時間
 * 2.通過計算還需等待時間,設置下一次添加令牌時間加上等待時間
 * 3.更新令牌剩餘數
 * @param requiredPermits 申請令牌數
 * @param nowMicros 秒錶經過時間
 * @return 如果未到下一次申請令牌時間,返回值爲下一次申請令牌時間,否則返回值爲當前值
 */
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
	//添加令牌,設置nextFreeTicketMicros = nowMicros
    this.resync(nowMicros);
    //設置返回值爲nowMicros
    long returnValue = this.nextFreeTicketMicros;
    //能夠花費的令牌數
    double storedPermitsToSpend = Math.min((double)requiredPermits, 						
    											this.storedPermits);
    //還差多少令牌數
    double freshPermits = (double)requiredPermits 
    	- storedPermitsToSpend;
    //SmoothBursty的storedPermitsToWaitTime()方法返回0
    //也就是等待時間waitMicros = (long)(freshPermits * this.stableIntervalMicros);
    long waitMicros = this.storedPermitsToWaitTime(this.storedPermits,
    												storedPermitsToSpend) 
    	+ (long)(freshPermits * this.stableIntervalMicros);
    //將nextFreeTicketMicros加上等待時間
    this.nextFreeTicketMicros = LongMath.saturatedAdd(this.nextFreeTicketMicros,
    	 waitMicros);
    //設置剩餘令牌數
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
}
總結上面的一系列方法——

如果已到下一次申請令牌時間,則不需要等待,設置下一次申請令牌時間爲(當前秒錶經過時間加上本次申請令牌需花費時間)
如果未到下一次申請令牌時間,則等待到下一次申請令牌時間,同時計算新的下次申請令牌時間

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章