如果某個接口可能出現突發情況,比如“秒殺”活動,那么很有可能因為突然爆發的訪問量造成系統奔潰,我們需要最這樣的接口進行限流。
在上一篇“限流算法”中,我們簡單提到了兩種限流方式:
1)(令牌桶、漏桶算法)限速率,例如:每 5r/1s = 1r/200ms 即一個請求以200毫秒的速率來執行;
2)(計數器方式)限制總數、或者單位時間內的總數,例如:設定總並發數的閥值,單位時間總並發數的閥值。
一、限制總並發數
我們可以采用java提供的atomicLong類來實現
atomicLong在java.util.concurrent.atomic包下,它直接繼承於number類,它是線程安全的。
atomicLong可以參考:http://www.cnblogs.com/lay2017/p/9066719.html
CountDownLatch可以參考:http://www.cnblogs.com/lay2017/p/9067756.html
我們將使用它來計數
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