1. 程式人生 > >限流(二)接口限流

限流(二)接口限流

() atom ted AC exception 狀態 ... 單位 容量

如果某個接口可能出現突發情況,比如“秒殺”活動,那麽很有可能因為突然爆發的訪問量造成系統奔潰,我們需要最這樣的接口進行限流。

在上一篇“限流算法”中,我們簡單提到了兩種限流方式:

1)(令牌桶、漏桶算法)限速率,例如:每 5r/1s = 1r/200ms 即一個請求以200毫秒的速率來執行;

2)(計數器方式)限制總數、或者單位時間內的總數,例如:設定總並發數的閥值,單位時間總並發數的閥值。

一、限制總並發數

我們可以采用java提供的atomicLong類來實現

atomicLong在java.util.concurrent.atomic包下,它直接繼承於number類,它是線程安全的。

我們將使用它來計數

public class AtomicDemo {
    // 計數
    public static AtomicLong atomicLong = new AtomicLong(0L);
    // 最大請求數量
    static int limit = 10;
    // 請求數量
    static int reqAmonut = 15;
    
    public static void main(String[] args) throws InterruptedException {
        // 多線程並發模擬
        final CountDownLatch latch = new
CountDownLatch(1); for (int i = 1; i <= reqAmonut; i++) { final int t = i; new Thread(new Runnable() { public void run() { try { latch.await(); // 計數器加1,並判斷最大請求數量 if
(atomicLong.getAndIncrement() > limit) { System.out.println(t + "線程:限流了"); return; } System.out.println(t + "線程:業務處理"); // 休眠1秒鐘,模擬業務處理 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 計數器減1 atomicLong.decrementAndGet(); } } }).start(); } latch.countDown(); } }

二、限制單位時間的總並發數

下面用谷歌的Guava依賴中的Cache線程安全)來完成單位時間的並發數限制,

Guava需要引入依賴:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>21.0</version>
</dependency>

具體邏輯如下:

1)根據當前的的時間戳(秒)做key,請求計數的值做value;

2)每個請求都通過時間戳來獲取計數值,並判斷是否超過限制。(即,1秒內的請求數量是否超過閥值

代碼如下:

public class AtomicDemo2 {
    // 計數
    public static AtomicLong atomicLong = new AtomicLong(0L);
    // 最大請求數量
    static int limit = 10;
    // 請求數量
    static int reqAmonut = 15;
    
    public static void main(String[] args) throws InterruptedException {
        // Guava的Cache來存儲計數器

        final LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder()
                                  .expireAfterWrite(1, TimeUnit.SECONDS)
                                  .build(new CacheLoader<Long, AtomicLong>(){                             @Override                              public AtomicLong load(Long key) throws Exception {                             return new AtomicLong(0L);                             }                              }); // 多線程並發模擬 final CountDownLatch latch = new CountDownLatch(1); for (int i = 1; i <= reqAmonut; i++) { final int t = i; new Thread(new Runnable() { public void run() { try { latch.await(); long currentSeconds = System.currentTimeMillis()/1000; // 從緩存中取值,並計數器+1 if (counter.get(currentSeconds).getAndIncrement() > limit) { System.out.println(t + "線程:限流了"); return; } System.out.println(t + "線程:業務處理"); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }).start(); } latch.countDown(); } }

三、限制接口的速率

以上兩種以較為簡單的計數器方式實現了限流,但是他們都只是限制了總數。也就是說,它們允許瞬間爆發的請求達到最大值,這有可能導致一些問題。

下面我們將使用Guava的 RateLimiter提供的令牌桶算法來實現限制速率,例如:1r/200ms

同樣需要引入依賴

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>21.0</version>
</dependency>

示例代碼:

public class GuavaDemo {
    // 每秒鐘5個令牌
    static RateLimiter limiter = RateLimiter.create(5);
    
    public static void main(String[] args) throws InterruptedException {
        final RateLimiter limiter2 = RateLimiter.create(5);
        for (int i = 0; i < 20; i++) {
            System.out.println(i + "-" + limiter2.acquire());
        }
    }
}
  

說明:

1)RateLimiter.create(5)表示創建一個容量為5的令牌桶,並且每秒鐘新增5個令牌,也就是每200毫秒新增1個令牌;

2)limiter2.acquire() 表示消費一個令牌,如果桶裏面沒有足夠的令牌那麽就進入等待。

輸出:

0.0
0.197729
0.192975
...

平均 1r/200ms的速率處理請求

RateLimiter允許突發超額,例如:

public class GuavaDemo {
    // 每秒鐘5個令牌
    static RateLimiter limiter = RateLimiter.create(5);
    
    public static void main(String[] args) throws InterruptedException {
        final RateLimiter limiter2 = RateLimiter.create(5);
        System.out.println(limiter2.acquire(10));
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
    }
}

輸出:

0.0
1.997777
0.194835
0.198466
0.195192
0.197448
0.196706

我們看到:

limiter2.acquire(10)

超額消費了10個令牌,而下一個消費需要等待超額消費的時間,所以等待了近2秒鐘的時間,而後又開始勻速處理請求

由於上面的方式允許突發,很多人可能擔心這種突發對於系統來說如果扛不住可能就造成崩潰。那針對這種情況,大家希望能夠從慢速到勻速地平滑過渡。Guava當然也提供了這樣的實現:

public class GuavaDemo {
    // 每秒鐘5個令牌
    static RateLimiter limiter = RateLimiter.create(5);
    
    public static void main(String[] args) throws InterruptedException {
        final RateLimiter limiter2 = RateLimiter.create(5, 1, TimeUnit.SECONDS);
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
        System.out.println(limiter2.acquire());
    }
}

輸出:

0.0
0.51798
0.353722
0.216954
0.195776
0.194903
0.194547

我們看到,速率從0.5慢慢地趨於0.2,平滑地過渡到了勻速狀態。

RateLimter 還提供了tryAcquire()方法來判斷是否有夠的令牌,並即時返回結果,如:

public class GuavaDemo {
    public static void main(String[] args) throws InterruptedException {
        final RateLimiter limiter = RateLimiter.create(5, 1, TimeUnit.SECONDS);
        for (int i = 0; i < 10; i++) {
            if (limiter.tryAcquire()) {
                System.out.println("處理業務");
            }else{
                System.out.println("限流了");
            }
        }
    }
}

輸出:

處理業務
限流了
限流了
限流了
限流了
限流了
限流了
限流了
限流了
限流了

以上,就是單實例系統的應用級接口限流方式。

參考:

http://jinnianshilongnian.iteye.com/blog/2305117

限流(二)接口限流