java各层级限流对比,面试官说:来谈谈限流-从概念到实现,一问你就懵逼了?...
后端服務的接口都是有訪問上限的,如果外部qps或并發量超過了訪問上限會導致應用癱瘓。所以一般都會對接口調用加上限流保護,防止超出預期的請求導致系統故障。
從限流類型來說一般來說分為兩種:并發數限流和qps限流,并發數限流就是限制同一時刻的最大并發請求數量,qps限流指的是限制一段時間內發生的請求個數。
從作用范圍的層次上來看分單機限流和分布式限流,前者是針對單機的,后者是針對集群的,他們的思想都是一樣的,只不過是范圍不一樣,本文分析的都是單機限流。
接下來我們看看并發數限流和qps限流。
并發數限流
并發數限流限制的是同一時刻的并發數,所以不考慮線程安全的話,我們只要用一個int變量就能實現,偽代碼如下:
int maxrequest=100;
int nowrequest=0;
public void request(){
if(nowrequest>=maxrequest){
return ;
}
nowrequest++;
//調用接口
try{
invokexxx();
}finally{
nowrequest--;
}
}
顯然,上述實現會有線程安全的問題,最直接的做法是加鎖:
int maxrequest=100;
int nowrequest=0;
public void request(){
if(nowrequest>=maxrequest){
return ;
}
synchronized(this){
if(nowrequest>=maxrequest){
return ;
}
nowrequest++;
}
//調用接口
try{
invokexxx();
}finally{
synchronized(this){
nowrequest--;
}
}
}
當然也可以用atomicinteger實現:
int maxrequest=100;
atomicinteger nowrequest=new atomicinteger(0);
public void request(){
for(;;){
int currentreq=nowrequest.get();
if(currentreq>=maxrequest){
return;
}
if(nowrequest.compareandset(currentreq,currentreq+1)){
break;
}
}
//調用接口
try{
invokexxx();
}finally{
nowrequest.decrementandget();
}
}
熟悉jdk并發包的同學會說干嘛這么麻煩,這不就是信號量(semaphore)做的事情嗎? 對的,其實最簡單的方法就是用信號量來實現:
int maxrequest=100;
semaphore reqsemaphore = new semaphore(maxrequest);
public void request(){
if(!reqsemaphore.tryacquire()){
return ;
}
//調用接口
try{
invokexxx();
}finally{
reqsemaphore.release();
}
}
條條大路通羅馬,并發數限流比較簡單,一般來說用信號量就好。
qps限流
qps限流限制的是一段時間內(一般指1秒)的請求個數。
計數器法
最簡單的做法用一個int型的count變量做計數器:請求前計數器+1,如超過閾值并且與第一個請求的間隔還在1s內,則限流。
偽代碼如下:
int maxqps=100;
int count;
long timestamp=system.currenttimemillis();
long interval=1000;
public synchronized boolean grant(){
long now=system.currenttimemillis();
if(now
count++;
return count
}else{
timestamp=now;
count=1;
return true;
}
}
該種方法實現起來很簡單,但其實是有臨界問題的,假如在第一秒的后500ms來了100個請求,第2秒的前500ms來了100個請求,那在這1秒內其實最大qps為200。如下圖:
計數器法會有臨界問題,主要還是統計的精度太低,這點可以通過滑動窗口算法解決
滑動窗口
我們用一個長度為10的數組表示1秒內的qps請求,數組每個元素對應了相應100ms內的請求數。用一個sum變量代碼當前1s的請求數。同時每隔100ms將淘汰過期的值。
偽代碼如下:
int maxqps=100;
atomicinteger[] count=new atomicinteger[10];
long timestamp=system.currenttimemillis();
long interval=1000;
atomicinteger sum;
volatile int index;
public void init(){
for(int i=0;i
count[i]=new atomicinteger(0);
}
sum=new atomicinteger(0);
}
public synchronized boolean grant(){
count[index].incrementandget();
return sum.incrementandget()
}
//每100ms執行一次
public void run(){
index=(index+1)%count.length;
int val=count[index].getandset(0);
sum.addandget(-val);
}
滑動窗口的窗口越小,則精度越高,相應的資源消耗也更高。
漏桶算法
漏桶算法思路是,有一個固定大小的桶,水(請求)忽快忽慢的進入到漏桶里,漏桶以一定的速度出水。當桶滿了之后會發生溢出。
在維基百科上可以看到,漏桶算法有兩種實現,一種是as a meter,另一種是as a queue。網上大多數文章都沒有提到其有兩種實現,且對這兩種概念混亂。
as a meter
第一種實現是和令牌桶等價的,只是表述角度不同。
偽代碼如下:
long timestamp=system.currenttimemillis();//上一次調用grant的時間
int bucketsize=100;//桶大小
int rate=10;//每ms流出多少請求
int count;//目前的水量
public synchronized boolean grant(){
long now = system.currenttimemillis();
if(now>timestamp){
count = math.max(0,count-(now-timestamp)*rate);
timestamp = now;
}
if(count+1<=bucketsize){
count++;
return true;
}else{
return false;
}
}
該種實現允許一段時間內的突發流量,比如初始時桶中沒有水,這時1ms內來了100個請求,這100個請求是不會被限流的,但之后每ms最多只能接受10個請求(比如下1ms又來了100個請求,那其中90個請求是會被限流的)。
其達到的效果和令牌桶一樣。
as a queue
第二種實現是用一個隊列實現,當請求到來時如果隊列沒滿則加入到隊列中,否則拒絕掉新的請求。同時會以恒定的速率從隊列中取出請求執行。
偽代碼如下:
queue queue=new linkedblockingqueue(100);
int gap;
int rate;
public synchronized boolean grant(request req){
if(!queue.offer(req)){return false;}
}
// 單獨線程執行
void consume(){
while(true){
for(int i=0;i
//執行請求
request req=queue.poll();
if(req==null){break;}
req.dorequest();
}
thread.sleep(gap);
}
}
對于該種算法,固定的限定了請求的速度,不允許流量突發的情況。
比如初始時桶是空的,這時1ms內來了100個請求,那只有前10個會被接受,其他的會被拒絕掉。注意與上文中as a meter實現的區別。
**不過,當桶的大小等于每個ticket流出的水大小時,第二種漏桶算法和第一種漏桶算法是等價的。**也就是說,as a queue是as a meter的一種特殊實現。如果你沒有理解這句話,你可以再看看上面as a meter的偽代碼,當bucketsize==rate時,請求速度就是恒定的,不允許突發流量。
令牌桶算法
令牌桶算法的思想就是,桶中最多有n個令牌,會以一定速率往桶中加令牌,每個請求都需要從令牌桶中取出相應的令牌才能放行,如果桶中沒有令牌則被限流。
令牌桶算法與上文的漏桶算法as a meter實現是等價的,能夠在限制數據的平均傳輸速率的同時還允許某種程度的突發傳輸。偽代碼:
int token;
int bucketsize;
int rate;
long timestamp=system.currenttimemillis();
public synchronized boolean grant(){
long now=system.currenttimemillis();
if(now>timestamp){
token=math.max(bucketsize,token+(timestamp-now)*rate);
timestamp=now;
}
if(token>0){
token--;
return true;
}else{
return false;
}
}
漏桶算法兩種實現和令牌桶算法的對比
as a meter的漏桶算法和令牌桶算法是一樣的,只是思想角度有所不同。
as a queue的漏桶算法能強行限制數據的傳輸速率,而令牌桶和as a meter漏桶則能夠在限制數據的平均傳輸速率的同時還允許某種程度的突發傳輸。
一般業界用的比較多的是令牌桶算法,像guava中的ratelimiter就是基于令牌桶算法實現的。當然不同的業務場景會有不同的需要,具體的選擇還是要結合場景。
end
本文介紹了后端系統中常用的限流算法,對于每種算法都有對應的偽代碼,結合偽代碼理解起來應該不難。但偽代碼中只是描述了大致思想,對于一些細節和效率問題并沒有關注,所以下篇文章將會分析常用限流api:guava的ratelimiter的源碼實現,讓讀者對于限流有個更清晰的認識。
本人免費整理了java高級資料,涵蓋了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo高并發分布式等教程,一共30g,需要自己領取。
希望與廣大網友互動??
點此進行留言吧!
總結
以上是生活随笔為你收集整理的java各层级限流对比,面试官说:来谈谈限流-从概念到实现,一问你就懵逼了?...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 广发diy信用卡额度多少 提额技巧全解析
- 下一篇: 大学生信用卡逾期原因分析 小心陷入不良信