限流:漏桶和令牌桶算法 單機實現


漏桶:漏桶可以看作是一個漏斗類似,水可以以任意速度流入,桶保存一定量的水,水以一定的速率流出。

 

 

 

令牌桶:桶會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。

 

 

 

從原理上看,令牌桶算法和漏桶算法是相反的,一個“進水”,一個是“漏水”。

在單機上的實現

漏桶

import java.time.LocalDateTime;

/**
 * 漏桶
 */
public class LeakyBucket {
    //流水速率  固定
    private double rate;
    //桶的大小
    private double burst;
    //最后更新時間
    private int refreshTime;
    //private Long refreshTime;
    //桶里面的水量
    private int water;

    public LeakyBucket(double rate,double burst){
        this.rate=rate;
        this.burst=burst;
    }

    /**
     * 刷新桶的水量
     */
    private void refreshWater(){
        //long now = System.currentTimeMillis(); //毫秒生成
        LocalDateTime time=LocalDateTime.now();  //每秒生成
        int now = time.getSecond();
        //現在時間-上次更新的時間   中間花費的時間(秒)*流水速率=流水量(處理的請求的數量)  通過上次水總量減去流水量等於現在的水量
        //如果流水量太多導致桶里都沒那么多水就應該置0, 所以通過math.max函數實現
        water = (int)Math.max(0,water-(now-refreshTime)*rate);
        //更新上次時間
        refreshTime = now;
    }

    /**
     * 獲取令牌
     */
    public synchronized boolean tryAcquire(){
        //刷新桶的水量
        refreshWater();
        //如果桶的水量小於桶的容量就可以添加進來
        if(water<burst){
            water++;
            return true;
        }else {
            return false;
        }
    }
}
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest {
    public static LeakyBucket leakyBucket = new LeakyBucket(10,100);
    public static void main(String[] args) {long start = System.currentTimeMillis();
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(leakyBucket.tryAcquire());
                }
            }).start();
        }
        System.out.println("總花費:"+(System.currentTimeMillis()-start));
        System.out.println("線程執行完畢");
    }
}

令牌桶

Google開源項目Guava中的RateLimiter使用的就是令牌桶控制算法,所以我們直接使用Guava即可實現。

加入依賴

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>
import com.google.common.util.concurrent.RateLimiter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 令牌桶
 */
public class TokenBucket {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    /**
     * permitsPerSecond為每秒生成的令牌
     *
     */
     /** 平衡穩定 
     * * 創建一個穩定輸出令牌的RateLimiter,保證了平均每秒不超過permitsPerSecond個請求
     * * 當請求到來的速度超過了permitsPerSecond,保證每秒只處理permitsPerSecond個請求
     * * 當這個RateLimiter使用不足(即請求到來速度小於permitsPerSecond),會囤積最多permitsPerSecond個請求
     */
    /**平衡預熱
     * 創建一個穩定輸出令牌的RateLimiter,保證了平均每秒不超過permitsPerSecond個請求
     * 還包含一個熱身期(warmup period),熱身期內,RateLimiter會平滑的將其釋放令牌的速率加大,直到起達到最大速率
     * 同樣,如果RateLimiter在熱身期沒有足夠的請求(unused),則起速率會逐漸降低到冷卻狀態
     * 設計這個的意圖是為了滿足那種資源提供方需要熱身時間,而不是每次訪問都能提供穩定速率的服務的情況(比如帶緩存服務,需要定期刷新緩存的)
     * 參數warmupPeriod和unit決定了其從冷卻狀態到達最大速率的時間
     */
    private static final RateLimiter rateLimiter = RateLimiter.create(10,2L, TimeUnit.SECONDS);
    //private static final RateLimiter rateLimiter = RateLimiter.create(10);

    /**
     * tryAcquire嘗試獲取permit,默認超時時間是0,意思是拿不到就立即返回false
     * @return
     */
    public String sayHello(){
        if(rateLimiter.tryAcquire()){    //一次拿一個
            System.out.println(sdf.format(new Date()));
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            return "no";
        }
        return "hello";
    }

    /**
     * acquire拿不到就等待,拿到為止
     * @return
     */
    public String sayHi(){
        rateLimiter.acquire(1);  //一次拿5個  意思就是生成10個令牌才去全部拿去給一個請求
        System.out.println(sdf.format(new Date()));
        return "hi";
    }
}
import java.util.concurrent.CountDownLatch;public class LeakyBucketTest {
    private static TokenBucket tokenBucket = new TokenBucket();
    public static void main(String[] args) {long start = System.currentTimeMillis();
        for (int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(tokenBucket.sayHi());
                }
            }).start();
        }
        System.out.println("總花費:"+(System.currentTimeMillis()-start));
        System.out.println("線程執行完畢");
    }
}

區別:

漏桶

漏桶的出水速度是恆定的,那么意味着如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢出)。

令牌桶

生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該算法可以在短時間內請求拿到大量令牌,而且拿令牌的過程並不是消耗很大的事情。

最后,不論是對於令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢出,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量,這是合理的,如果因為極少部分流量需要保證的話,那么就可能導致系統達到極限而掛掉,得不償失。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM