常用介面限流功能實現基於Guava設計
Guava是一組核心庫,包括新的集合型別(例如multimap和multiset),不可變集合,圖形庫,函式型別,記憶體快取以及用於併發,I / O,雜湊,基元的API /實用程式,反射,字串處理等等!
本示例只使用了Guava工具包的RateLimiter類,進行API的限流。
限流簡介:
限流中的“流”字該如何解讀呢?要限制的指標到底是什麼?不同的場景對“流”的定義也是不同的,可以是網路流量,頻寬,每秒處理的事務數 (TPS),每秒請求數 (hits per second),併發請求數,甚至還可能是業務上的某個指標,比如使用者在某段時間內允許的最多請求簡訊驗證碼次數。
從保證系統穩定可用的角度考量,對於微服務系統來說,最好的一個限流指標是:併發請求數。通過限制併發處理的請求數目,可以限制任何時刻都不會有過多的請求在消耗資源,比如:我們通過配置 web 容器中 servlet worker 執行緒數目為 200,則任何時刻最多都只有 200 個請求在處理,超過的請求都會被阻塞排隊。
假如一個介面請求量因為某些原因突然漲到之前的10倍,沒多久該介面幾乎不可使用,並引發連鎖反應導致整個系統崩潰。如何應對這種情況呢?生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的裝置,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的介面也需要安裝上“保險絲”,以防止非預期的請求對系統壓力過大而引起的系統癱瘓,當流量過大時,可以採取拒絕或者引流等機制。
常見的限流演算法有:
- 漏桶演算法
- 令牌桶演算法
- 漏桶演算法
漏桶演算法大概意思是請求進入到漏桶中,漏桶以一定的速率漏水。當請求過多時,水直接溢位。可以看出,漏桶演算法可以強制限制資料的傳輸速度。
- 令牌桶演算法
令牌桶演算法的原理是系統以一定速率向桶中放入令牌,如果有請求時,請求會從桶中取出令牌,如果能取到令牌,則可以繼續完成請求,否則等待或者拒絕服務。這種演算法可以應對突發程式的請求,因此比漏桶演算法好。
在Wikipedia上,令牌桶演算法是這麼描述的:
- 每秒會有r個令牌放入桶中,或者說,每過1/r 秒桶中增加一個令牌
- 桶中最多存放b個令牌,如果桶滿了,新放入的令牌會被丟棄
- 當一個n位元組的資料包到達時,消耗n個令牌,然後傳送該資料包
- 如果桶中可用令牌小於n,則該資料包將被快取或丟棄
RateLimiter
Guava中開源出來一個令牌桶演算法的工具類RateLimiter,可以輕鬆實現限流的工作。RateLimiter對簡單的令牌桶演算法做了一些工程上的優化,具體的實現是SmoothBursty。需要注意的是,RateLimiter的另一個實現SmoothWarmingUp,就不是令牌桶了,而是漏桶演算法。也許是出於簡單起見,RateLimiter中的時間視窗能且僅能為1S,如果想搞其他時間單位的限流,只能另外造輪子。
RateLimiter有一個有趣的特性是[前人挖坑後人跳],也就是說RateLimiter允許某次請求拿走了超出剩餘令牌數的令牌,但是下一次請求將為此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌為止。這裡面就涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它走掉後面的請求等一等呢?Guava的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說。
並且構建了一個自定義註解,方便鬆耦合,靈活的對服務進行限流。
/**
* The entry point of application.
* <p>
* RateLimiter有一個有趣的特性是[前人挖坑後人跳],也就是說RateLimiter允許某次請求拿走了超出剩餘令牌數的令牌,
* 但是下一次請求將為此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌為止。
* 這裡面就涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它走掉後面的請求等一等呢?
* Guava的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說。
*/
@Test
public void advanceConsumerTest() {
//每秒產生2個令牌
RateLimiter rateLimiter = RateLimiter.create(2);
//獲取令牌,返回獲取令牌所需等待的時間,獲取太多,導致後面得等虧損的令牌補上才能獲取到。
System.out.println(rateLimiter.acquire(10));
System.out.println(rateLimiter.tryAcquire(2, 2, TimeUnit.SECONDS));
System.out.println(rateLimiter.acquire(2));
System.out.println(rateLimiter.acquire(1));
}
結果如下,可以看到,RateLimiter每秒只能產生2個令牌,而第一獲取10個的話,後面的就需要用5秒的時間補上空缺。
0.0
false
4.994628
0.995124
下面通過一個例子,測試100個併發下,限流起到的效果
@Test
public void rateLimitTest() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i <= 100; i++) {
Business business = new Business(countDownLatch);
business.start();
}
countDownLatch.countDown();
//等待結果處理,有隻設了10個令牌,所以,只有10個請求有效。
TimeUnit.SECONDS.sleep(10);
System.out.println("所有模擬請求結束 at " + new Date());
}
class Business extends Thread {
CountDownLatch countDownLatch;
public Business(CountDownLatch latch) {
this.countDownLatch = latch;
}
@Override
public void run() {
try {
countDownLatch.await();
if (rateLimiterService.tryAcquire()) {
//模擬業務
TimeUnit.SECONDS.sleep(3);
System.out.println("成功處理業務" + new Date());
} else {
System.out.println("系統繁忙!請稍後再試!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行結果,只有10個請求獲取到令牌,成功執行,其他的都直接返回
.....
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
系統繁忙!請稍後再試!
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
成功處理業務Tue Nov 20 23:45:23 CST 2018
所有模擬請求結束 at Tue Nov 20 23:45:30 CST 2018
最後,為了方便日常使用,我還特定的設計了一個自定義註解,返回簡單定義達到效果,正所謂偷懶使人進步。
這裡貼出基於註解的設計:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiterAnnotation {
/**
* 限流服務名
*
* @return
*/
String name();
/**
* 每秒限流次數
*
* @return
*/
double count();
}
切面實現類
@Aspect
@Component
public class RateLimiterAnnotationAspect {
private ConcurrentMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
/**
* Before.
*
* @param point the point
*/
@Before("@annotation(com.dashuai.learning.ratelimiter.annotation.RateLimiterAnnotation)")
public void before(JoinPoint point) {
RateLimiterAnnotation rateLimiterAnnotation = this.getAnnotation(point, RateLimiterAnnotation.class);
double rateLimitCount = rateLimiterAnnotation.count();
String rateLimitName = rateLimiterAnnotation.name();
if (rateLimiterMap.get(rateLimitName) == null) {
rateLimiterMap.put(rateLimitName, RateLimiter.create(rateLimitCount));
}
rateLimiterMap.get(rateLimitName).acquire();
}
private <T extends Annotation> T getAnnotation(JoinPoint pjp, Class<T> clazz) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
return method.getAnnotation(clazz);
}
}
使用:
@Service
public class AopTestServiceImpl implements AopTestService {
@Override
@RateLimiterAnnotation(name = "v1", count = 5.0)
public String testRateLimiter(Double count, String context) {
System.out.println(count + " " + context);
return "測試";
}
@Override
@RateLimiterAnnotation(name = "v2", count = 7.0)
public String testRateLimiterv2(Double count, String context) {
System.out.println("V2版本發出:" + count + " " + context);
return "測試第二個";
}
}
設計思路較簡單,通過一個map儲存各個服務的限流數,在通過AOP切面前置判斷,達到一個限流效果。
本文原始碼以上傳github:
https://github.com/liaozihong/SpringBoot-Learning/tree/master/SpringBoot-RateLimiter
另外,一開源限流框架值得研究:
https://github.com/wangzheng0822/ratelimiter4j
參考連結:
https://wizardforcel.gitbooks.io/guava-tutorial/