目錄
一、引入
二、快速上手
2.1、導入依賴
2.2、第一個示例
三、獲取許可
3.1、非阻塞式獲取
3.2、阻塞式獲取
四、存在的問題
4.1、集群限流
一、引入
在程序中,我們經常會用到限流,比如接口調用的頻率限制。
server端提供api給clients進行調用,如果某個client調用api的頻率過高,造成server端的負載升高,超過server端的上限,那么很有可能導致server端不可用,從而影響所有的調用方。
限制頻率,可以在client端做,也可以在server端做,但是目前一般都是在server端做,同時client一般也會調整調用頻率。
至於怎么限流,網上很多的資料,這里就不闡述了,主要介紹一下使用Guava RateLimiter來實現限流。
二、快速上手
2.1、導入依賴
Guava RateLimiter是Guava的一部分,所以直接導入Guava的依賴即可。
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.0-jre</version> </dependency>
2.2、第一個示例
簡單看一下下面的用法
package cn.ganlixin.guava; import com.google.common.util.concurrent.RateLimiter; import org.junit.Test; import java.time.LocalTime; public class UserRateLimiter { @Test public void testSimple() throws InterruptedException { // 創建一個限流器(每秒限制流量為5個) RateLimiter rateLimiter = RateLimiter.create(5.0); for (int i = 0; i < 10; i++) { if (rateLimiter.tryAcquire()) { System.out.println(LocalTime.now() + " 通過"); } else { System.out.println(LocalTime.now() + " 被限流"); } Thread.sleep(100L); } } }
上面的示例代碼中,創建了一個限流器,每秒最多允許5個流量,這個流量有個專業的稱呼,叫“許可”(准許調用),也就是5個許可。
調用tryAcquire()會嘗試獲取1個許可,如果獲取到了,返回true,表示未被限流;否則返回false,表示沒有獲取到許可,被限流了。
如此就可以實現流量控制了。
三、獲取許可
獲取許可,有非阻塞和阻塞式獲取。
if (獲得許可) { 執行代碼塊 1 } else { 執行代碼塊 2 }
非阻塞式:嘗試獲取許可,如果獲取到許可,則執行代碼塊1,如果沒有獲取到就認為被限流,則執行代碼塊2;
阻塞式:嘗試獲取許可,獲取到則執行代碼塊1,沒有獲取到,則阻塞,等待獲取到許可后,執行代碼塊1,注意不會執行代碼塊2;
3.1、非阻塞獲取許可
前面使用介紹了,可以使用RateLimiter類可以使用create方法,創建多個許可,每個許可就是一個令牌,拿到令牌,才可以執行操作(通過),否則就是被限流(阻止)。
使用tryAcquire()方法,是不阻塞的嘗試獲取令牌,但是他有多個重載方法,有不同的參數。
// 嘗試獲取1個許可,如果獲取到,則返回true,否則返回false boolean tryAcquire(); // 嘗試獲取多個許可,如果獲取到,則返回true,否則返回false boolean tryAcquire(int permits) // 示例tryAcquire(3) // 在timeout時間內,嘗試獲取1個許可,如果獲取到,則返回true,否則返回false tryAcquire(Duration timeout); // 示例:tryAcquire(Duration.ofSeconds(3)) tryAcquire(long timeout, TimeUnit unit); // 示例:tryAcquire(3, TimeUnit.SECONDS); // 在timeout時間內,嘗試獲取多個許可,如果獲取到,則返回true,否則返回false tryAcquire(int permits, Duration timeout) tryAcquire(int permits, long timeout, TimeUnit unit)
3.2、阻塞式獲取許可
阻塞式獲取,調用的方法是acquire
package cn.ganlixin.guava; import com.google.common.util.concurrent.RateLimiter; import org.junit.Test; import java.time.LocalTime; public class UserRateLimiter { @Test public void testAcquire() { RateLimiter rateLimiter = RateLimiter.create(2); for (int i = 0; i < 10; i++) { double sleep = rateLimiter.acquire(); System.out.println("now: " + LocalTime.now() + " sleep: " + sleep); } } }
運行輸出如下:
now: 16:59:51.879 sleep: 0.0 now: 16:59:52.289 sleep: 0.403329 now: 16:59:52.784 sleep: 0.492976 now: 16:59:53.285 sleep: 0.499409 now: 16:59:53.789 sleep: 0.498335 now: 16:59:54.285 sleep: 0.494628 now: 16:59:54.786 sleep: 0.49857 now: 16:59:55.289 sleep: 0.496816 now: 16:59:55.784 sleep: 0.494352 now: 16:59:56.288 sleep: 0.499635
acquire也有重載方法:
// 嘗試阻塞獲取多個許可 acquire(int permits)
四、存在的問題
Guava解決的問題,一般都是單機的,以限流為例,使用guava限流,只做到了單機限流,但是我們的服務一般都會由多台機器(集群),雖然我們可以通過計算單機和集群的比例,來設置限流數量,但是有幾個問題:
1、機器增減時,要保證總流量保持不變,就需要修改每一台機器的流量限制,這個不是很方便;
2、Guava的限流器,並不是公平的,至於什么是公平和非公平,可以參考:
4.1 集群限流
guava的RateLimiter,限流是通過獲取“許可”來控制流量的,只不過是單機管理自己的許可。
如果將所有機器的“許可”匯集到一個地方,所有機器都從這個地方獲取許可,不就可以實現集群限流嗎?
可以使用redis來保存所有機器的“許可”。
這樣做,可以實現集群限流,但不能保證單機的流量限制,其實對於現在的微服務來說,請求被平均分給所有機器,是服務平台的問題,可以不用關心這個問題。