一.項目背景
最近項目中需要進行接口保護,防止高並發的情況把系統搞崩,因此需要對一個查詢接口進行限流,主要的目的就是限制單位時間內請求此查詢的次數,例如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循環,而是直接拋出異常或者返回,來拒絕這次執行(查詢),這樣便可以節省資源。
