工作中對外提供的API 介面設計都要考慮限流,如果不考慮限流,會成系統的連鎖反應,輕者響應緩慢,重者系統當機,整個業務線崩潰,如何應對這種情況呢,我們可以對請求進行引流或者直接拒絕等操作,保持系統的可用性和穩定性,防止因流量暴增而導致的系統執行緩慢或當機。
在開發高併發系統時有三把利器用來保護系統:快取、降級和限流
快取:快取的目的是提升系統訪問速度和增大系統處理容量
降級:降級是當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放伺服器資源以保證核心任務的正常執行
限流:限流的目的是通過對併發訪問/請求進行限速,或者對一個時間視窗內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理
限流演算法
常用的限流演算法有令牌桶和和漏桶,而Google開源專案Guava中的RateLimiter使用的就是令牌桶控制演算法。
漏桶演算法
把請求比作是水,水來了都先放進桶裡,並以限定的速度出水,當水來得過猛而出水不夠快時就會導致水直接溢位,即拒絕服務。
漏斗有一個進水口 和 一個出水口,出水口以一定速率出水,並且有一個最大出水速率:
在漏斗中沒有水的時候,
- 如果進水速率小於等於最大出水速率,那麼,出水速率等於進水速率,此時,不會積水
- 如果進水速率大於最大出水速率,那麼,漏斗以最大速率出水,此時,多餘的水會積在漏斗中
在漏斗中有水的時候
- 出水口以最大速率出水
- 如果漏斗未滿,且有進水的話,那麼這些水會積在漏斗中
- 如果漏斗已滿,且有進水的話,那麼這些水會溢位到漏斗之外
令牌桶演算法
對於很多應用場景來說,除了要求能夠限制資料的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶演算法可能就不合適了,令牌桶演算法更為適合。
令牌桶演算法的原理是系統以恆定的速率產生令牌,然後把令牌放到令牌桶中,令牌桶有一個容量,當令牌桶滿了的時候,再向其中放令牌,那麼多餘的令牌會被丟棄;當想要處理一個請求的時候,需要從令牌桶中取出一個令牌,如果此時令牌桶中沒有令牌,那麼則拒絕該請求。
RateLimiter 用法
新增依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
<!-- or, for Android: -->
<version>26.0-android</version>
</dependency>
複製程式碼
public class Test {
public static void main(String[] args) {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100));
// 指定每秒放1個令牌
RateLimiter limiter = RateLimiter.create(1);
for (int i = 1; i < 50; i++) {
// 請求RateLimiter, 超過permits會被阻塞
//acquire(int permits)函式主要用於獲取permits個令牌,並計算需要等待多長時間,進而掛起等待,並將該值返回
Double acquire = null;
if (i == 1) {
acquire = limiter.acquire(1);
} else if (i == 2) {
acquire = limiter.acquire(10);
} else if (i == 3) {
acquire = limiter.acquire(2);
} else if (i == 4) {
acquire = limiter.acquire(20);
} else {
acquire = limiter.acquire(2);
}
executorService.submit(new Task("獲取令牌成功,獲取耗:" + acquire + " 第 " + i + " 個任務執行"));
}
}
}
class Task implements Runnable {
String str;
public Task(String str) {
this.str = str;
}
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + str);
}
}
複製程式碼
響應
2018-08-11 00:26:22.953 | pool-1-thread-1獲取令牌成功,獲取耗:0.0 第 1 個任務執行
2018-08-11 00:26:23.923 | pool-1-thread-2獲取令牌成功,獲取耗:0.98925 第 2 個任務執行
2018-08-11 00:26:33.920 | pool-1-thread-3獲取令牌成功,獲取耗:9.996993 第 3 個任務執行
2018-08-11 00:26:35.920 | pool-1-thread-4獲取令牌成功,獲取耗:1.999051 第 4 個任務執行
2018-08-11 00:26:55.920 | pool-1-thread-5獲取令牌成功,獲取耗:19.999726 第 5 個任務執行
2018-08-11 00:26:57.920 | pool-1-thread-6獲取令牌成功,獲取耗:1.999139 第 6 個任務執行
2018-08-11 00:26:59.920 | pool-1-thread-7獲取令牌成功,獲取耗:1.999806 第 7 個任務執行
2018-08-11 00:27:01.919 | pool-1-thread-8獲取令牌成功,獲取耗:1.999433 第 8 個任務執行
複製程式碼
acquire
函式主要用於獲取permits個令牌,並計算需要等待多長時間,進而掛起等待,並將該值返回
一個RateLimiter主要定義了發放permits的速率。如果沒有額外的配置,permits將以固定的速度分配,單位是每秒多少permits。預設情況下,Permits將會被穩定的平緩的發放。
預消費能力
從輸出結果可以看出,指定每秒放1個令牌,RateLimiter具有預消費的能力:
acquire 1
時,並沒有任何等待 0.0 秒 直接預消費了1個令牌
acquire 10
時,由於之前預消費了 1 個令牌,故而等待了1秒,之後又預消費了10個令牌
acquire 2
時,由於之前預消費了 10 個令牌,故而等待了10秒,之後又預消費了2個令牌
acquire 20
時,由於之前預消費了 2 個令牌,故而等待了2秒,之後又預消費了20個令牌
acquire 2
時,由於之前預消費了 20 個令牌,故而等待了20秒,之後又預消費了2個令牌
acquire 2
時,由於之前預消費了 2 個令牌,故而等待了2秒,之後又預消費了2個令牌
acquire 2
時 …..
通俗的講「前人_挖坑_後人跳」,也就說上一次請求獲取的permit數越多,那麼下一次再獲取授權時更待的時候會更長,反之,如果上一次獲取的少,那麼時間向後推移的就少,下一次獲得許可的時間更短。可見,都是有代價的。正所謂:要浪漫就要付出代價。馬上就七夕了,浪漫的代價可能要花錢啊,單身狗們。
令牌桶演算法VS漏桶演算法
漏桶
漏桶的出水速度是恆定的,那麼意味著如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢位)。
令牌桶
生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該演算法可以在短時間內請求拿到大量令牌,而且拿令牌的過程並不是消耗很大的事情。
最後
不論是對於令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢位,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量,這是合理的,如果因為極少部分流量需要保證的話,那麼就可能導致系統達到極限而掛掉,得不償失。
本文講的單機的限流,是JVM級別的的限流,所有的令牌生成都是在記憶體中,在分散式環境下不能直接這麼用,可用使redis限流。