基於Guava的RateLimiter設計常用接口限流功能

Guava是一組核心庫,包括新的集合類型(例如multimap和multiset),不可變集合,圖形庫,函數類型,內存緩存以及用於併發,I / O,散列,基元的API /實用程序,反射,字符串處理等等!

本示例只使用了Guava工具包的RateLimiter類,進行API的限流。
限流簡介:
限流中的“流”字該如何解讀呢?要限制的指標到底是什麼?不同的場景對“流”的定義也是不同的,可以是網絡流量,帶寬,每秒處理的事務數 (TPS),每秒請求數 (hits per second),併發請求數,甚至還可能是業務上的某個指標,比如用戶在某段時間內允許的最多請求短信驗證碼次數。

從保證系統穩定可用的角度考量,對於微服務系統來說,最好的一個限流指標是:併發請求數。通過限制併發處理的請求數目,可以限制任何時刻都不會有過多的請求在消耗資源,比如:我們通過配置 web 容器中 servlet worker 線程數目爲 200,則任何時刻最多都只有 200 個請求在處理,超過的請求都會被阻塞排隊。

假如一個接口請求量因爲某些原因突然漲到之前的10倍,沒多久該接口幾乎不可使用,並引發連鎖反應導致整個系統崩潰。如何應對這種情況呢?生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預期的請求對系統壓力過大而引起的系統癱瘓,當流量過大時,可以採取拒絕或者引流等機制。

常見的限流算法有:

  • 漏桶算法
  • 令牌桶算法
  1. 漏桶算法
    漏桶算法大概意思是請求進入到漏桶中,漏桶以一定的速率漏水。當請求過多時,水直接溢出。可以看出,漏桶算法可以強制限制數據的傳輸速度。
  2. 令牌桶算法
    令牌桶算法的原理是系統以一定速率向桶中放入令牌,如果有請求時,請求會從桶中取出令牌,如果能取到令牌,則可以繼續完成請求,否則等待或者拒絕服務。這種算法可以應對突發程序的請求,因此比漏桶算法好。

在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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章