Google Guava緩存實現接口的限流


一.項目背景

最近項目中需要進行接口保護,防止高並發的情況把系統搞崩,因此需要對一個查詢接口進行限流,主要的目的就是限制單位時間內請求此查詢的次數,例如1000次,來保護接口。
參考了 開濤的博客聊聊高並發系統限流特技 ,學習了其中利用Google Guava緩存實現限流的技巧,在網上也查到了很多關於Google Guava緩存的博客,學到了好多,推薦一個博客文章:http://ifeve.com/google-guava-cachesexplained/,關於Google Guava緩存的更多細節或者技術,這篇文章講的很詳細;
這里我們並不是用緩存來優化查詢,而是利用緩存,存儲一個計數器,然后用這個計數器來實現限流。

二.效果實驗

static LoadingCache<Long, AtomicLong> count = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(new CacheLoader<Long, AtomicLong>() {
            @Override
        public AtomicLong load(Long o) throws Exception {
            //System.out.println("Load call!");
            return new AtomicLong(0L);
        }
    });

上面,我們通過CacheBuilder來新建一個LoadingCache緩存對象count,然后設置其有效時間為兩秒,即每兩秒鍾刷新一次;緩存中,key為一個long型的時間戳類型,value是一個計數器,使用原子性的AtomicLong保證自增和自減操作的原子性, 每次查詢緩存時如果不能命中,即查詢的時間戳不在緩存中,則重新加載緩存,執行load將當前的時間戳的計數值初始化為0。這樣對於每一秒的時間戳,能計算這一秒內執行的次數,從而達到限流的目的;
這是要執行的一個getCounter方法:

public class Counter {
    static int counter = 0;
    public static int getCounter() throws Exception{
        return counter++;
    }
}

現在我們創建多個線程來執行這個方法:

ublic class Test {

    public static void main(String args[]) throws Exception
    {
        for(int i = 0;i<100;i++)
        {
            new Thread(){
                @Override
                public void run() {
                    try {
                        System.out.println(Counter.getCounter());
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }
            }.start();
        }

    }
}

這樣執行的話,執行結果很簡單,就是很快地執行這個for循環,迅速打印0到99折100個數,不再貼出。
這里的for循環執行100個進程時間是很快的,那么現在我們要限制每秒只能有10個線程來執行getCounter()方法,該怎么辦呢,上面講的限流方法就派上用場了:

public class Counter {
    static LoadingCache<Long, AtomicLong> count = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(new CacheLoader<Long, AtomicLong>() {
            @Override
        public AtomicLong load(Long o) throws Exception {
                System.out.println("Load call!");
            return new AtomicLong(0L);
        }
    });
    static long limits = 10;
    static int counter = 0;
    public static synchronized int getCounter() throws Exception{
        while (true)
        {
            //獲取當前的時間戳作為key
            Long currentSeconds = System.currentTimeMillis() / 1000;
            if (count.get(currentSeconds).getAndIncrement() > limits) {
                continue;
            }
            return counter++;
        }
    }
}

這樣一來,就可以限制每秒的執行數了。對於每個線程,獲取當前時間戳,如果當前時間(當前這1秒)內有超過10個線程正在執行,那么這個進程一直在這里循環,直到下一秒,或者更靠后的時間,重新加載,執行load,將新的時間戳的計數值重新為0。
執行結果:
這里寫圖片描述
每秒執行11個(因為從0開始),每一秒之后,load方法會執行一次;

為了更加直觀,我們可以讓每個for循環sleep一段時間:

public class Test {

    public static void main(String args[]) throws Exception
    {
        for(int i = 0;i<100;i++)
        {
            new Thread(){
                @Override
                public void run() {
                    try {
                        System.out.println(Counter.getCounter());
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }
            }.start();
            Thread.sleep(100);
        }

    }
}

在上述這樣的情況下,一個線程如果遇到當前時間正在執行的線程超過limit值就會一直在while循環,這樣會浪費大量的資源,我們在做限流的時候,如果出現這種情況,可以不進行while循環,而是直接拋出異常或者返回,來拒絕這次執行(查詢),這樣便可以節省資源。


免責聲明!

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



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