背景
在說限流方案之前我們需要知道在什麼樣的場景下需要限流,以前我們都是應用部署在單臺機器上,但是隨着流量的增大單臺服務已經扛不住壓力了,所以就變成了新的的集羣部署,將流量壓力分擔到多臺機器上,來提高吞吐量,但是互聯網企業嘛,在一些節日或者特殊的日子裏會搞一些搶購的活動,這個併發壓力可能是平時的很多倍吧,按照正常的思路來說我們增加機器橫向擴展吧,可是平時的話流量沒有這麼大,購買這些機器其實是非常浪費的,而且我們的搶購只是流量大,但是實際的有效流量是很少的,所以限流方案出來了,guava的工具類中RateLimiter就提供了這樣的功能,下面來看下幾種限流方案。
一、限流方案
1、計數器方式
採用AtomicInteger:
使用AomicInteger來進行統計當前正在併發執行的次數,如果超過域值就簡單粗暴的直接響應給用戶,說明系統繁忙,請稍後再試或其它跟業務相關的信息。
弊端:使用 AomicInteger 簡單粗暴超過域值就拒絕請求,可能只是瞬時的請求量高,也會拒絕請求。
採用令牌Semaphore:
使用Semaphore信號量來控制併發執行的次數,如果超過域值信號量,則進入阻塞隊列中排隊等待獲取信號量進行執行。如果阻塞隊列中排隊的請求過多超出系統處理能力,則可以在拒絕請求。
相對Atomic優點:如果是瞬時的高併發,可以使請求在阻塞隊列中排隊,而不是馬上拒絕請求,從而達到一個流量削峯的目的。
採用ThreadPoolExecutor java線程池:
固定線程池大小,超出固定先線程池和最大的線程數,拒絕線程請求;
2、令牌桶方式
令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。先有一個木桶,系統按照固定速度,往桶裏加入Token,如果桶已經滿了就不再添加。當有請求到來時,會各自拿走一個Token,取到Token 才能繼續進行請求處理,沒有Token 就拒絕服務。
這裏如果一段時間沒有請求時,桶內就會積累一些Token,下次一旦有突發流量,只要Token 足夠,也能一次處理,所以令牌桶算法的特點是允許突發流量。
我們看一個例子,看看令牌桶如何允許突發流量,假如令牌則按照每秒5 個的速度放入令牌桶,桶中最多存放20 個令牌,那系統可以支持兩種類型的請求流量,一種是允許持續的每秒處理5 個請求,第二種是每隔4 秒,等桶中20 個令牌攢滿後,就可以處理一次有20 個請求的突發情況。
令牌桶的方案設計:
使用guava提供工具庫裏的RateLimiter類(內部採用令牌捅算法實現)進行限流
3、漏桶算法
一個固定容量的漏桶,按照常量固定速率流出水滴;
先想象有一個木桶,新請求就像水滴一樣,不斷地滴進來,水滴進來的速度是不確定的,有時會快一點,有時會慢一點,同時桶底下有個洞,可以按照固定的速度把水漏走,如果水進來的速度比漏走的快,桶就會滿了,桶滿了水就會漫出來,對應的就是拒絕請求。
漏桶算法的主要特點是可以平滑網絡上的突發流量,請求可以被整形成穩定的流量。
二、實現(令牌通方式)
1、代碼
public static void main(String[] args) throws InterruptedException {
RateLimiter limiter = RateLimiter.create(5);
long start = System.currentTimeMillis();
System.out.println("start:"+start);
for (int i = 0; i <20 ; i++) {
Thread.sleep(10);
boolean getToken =limiter.tryAcquire();
if (getToken){
System.out.println("獲取了令牌====="+(i+1));
}
}
long end = System.currentTimeMillis();
System.out.println("end:"+end);
System.out.println("花費時間:"+(end-start)+"毫秒");
RateLimiter limiter1 = RateLimiter.create(1);
System.out.println(limiter1.acquire(10));
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
}
2、結果
start:1575013943433
獲取了令牌=====1
獲取了令牌=====19
end:1575013943652
花費時間:219毫秒
0.0
9.998836
0.99797
1.000141
0.998961
0.99968
0.999161
0.999291
Disconnected from the target VM, address: '127.0.0.1:62636', transport: 'socket'
0.999234
Process finished with exit code 0