後端服務的接口都是有訪問上限的,若是外部QPS或併發量超過了訪問上限會致使應用癱瘓。因此通常都會對接口調用加上限流保護,防止超出預期的請求致使系統故障。算法
從限流類型來講通常來講分爲兩種:併發數限流和qps限流,併發數限流就是限制同一時刻的最大併發請求數量,qps限流指的是限制一段時間內發生的請求個數。後端
從做用範圍的層次上來看分單機限流和分佈式限流,前者是針對單機的,後者是針對集羣的,他們的思想都是同樣的,只不過是範圍不同,本文分析的都是單機限流。數組
接下來咱們看看併發數限流和QPS限流。安全
併發數限流限制的是同一時刻的併發數,因此不考慮線程安全的話,咱們只要用一個int變量就能實現,僞代碼以下:bash
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)作的事情嗎? 對的,其實最簡單的方法就是用信號量來實現:ui
int maxRequest=100;
Semaphore reqSemaphore = new Semaphore(maxRequest);
public void request(){
if(!reqSemaphore.tryAcquire()){
return ;
}
//調用接口
try{
invokeXXX();
}finally{
reqSemaphore.release();
}
}複製代碼
條條大路通羅馬,併發數限流比較簡單,通常來講用信號量就好。this
QPS限流限制的是一段時間內(通常指1秒)的請求個數。spa
最簡單的作法用一個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<timeStamp+interval){
count++;
return count<maxQps;
}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.length;i++){
count[i]=new AtomicInteger(0);
}
sum=new AtomicInteger(0);
}
public synchronized boolean grant(){
count[index].incrementAndGet();
return sum.incrementAndGet()<maxQps;
}
//每100ms執行一次
public void run(){
index=(index+1)%count.length;
int val=count[index].getAndSet(0);
sum.addAndGet(-val);
}複製代碼
滑動窗口的窗口越小,則精度越高,相應的資源消耗也更高。
漏桶算法思路是,有一個固定大小的桶,水(請求)忽快忽慢的進入到漏桶裏,漏桶以必定的速度出水。當桶滿了以後會發生溢出。
在維基百科上能夠看到,漏桶算法有兩種實現,一種是as a meter
,另外一種是as a queue
。網上大多數文章都沒有提到其有兩種實現,且對這兩種概念混亂。
第一種實現是和令牌桶等價的,只是表述角度不一樣。
僞代碼以下:
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個請求是會被限流的)。
其達到的效果和令牌桶同樣。
第二種實現是用一個隊列實現,當請求到來時若是隊列沒滿則加入到隊列中,不然拒絕掉新的請求。同時會以恆定的速率從隊列中取出請求執行。
僞代碼以下:
Queue<Request> 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<rate;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
就是基於令牌桶算法實現的。固然不一樣的業務場景會有不一樣的須要,具體的選擇仍是要結合場景。
本文介紹了後端系統中經常使用的限流算法,對於每種算法都有對應的僞代碼,結合僞代碼理解起來應該不難。但僞代碼中只是描述了大體思想,對於一些細節和效率問題並無關注。