SpringBoot進階教程(六十七)RateLimiter限流

在上一篇文章nginx限流配置中,我們介紹瞭如何使用nginx限流,這篇文章介紹另外一種限流方式---RateLimiter。

v限流背景

在早期的計算機領域,限流技術(time limiting)被用作控制網絡接口收發通信數據的速率。 可以用來優化性能,減少延遲和提高帶寬等。 現在在互聯網領域,也借鑑了這個概念, 用來爲服務控制請求的速率, 如果雙十一的限流, 12306的搶票等。 即使在細粒度的軟件架構中,也有類似的概念。

系統使用下游資源時,需要考慮下游對資源受限、處理能力,在下游資源無法或者短時間內無法提升處理性能的情況下,可以使用限流器或者類似保護機制,避免下游服務崩潰造成整體服務的不可用。

v限流算法

常見限流算法有兩種:漏桶算法和令牌桶算法。

2.1 漏桶算法

漏桶算法(Leaky Bucket)是網絡世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時經常使用的一種算法,它的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便爲網絡提供一個穩定的流量。

漏桶可以看作是一個帶有常量服務時間的單服務器隊列,如果漏桶(包緩存)溢出,那麼數據包會被丟棄。 在網絡中,漏桶算法可以控制端口的流量輸出速率,平滑網絡上的突發流量,實現流量整形,從而爲網絡提供一個穩定的流量。

如下圖所示,把請求比作是水,水來了都先放進桶裏,並以限定的速度出水,當水來得過猛而出水不夠快時就會導致水直接溢出,即拒絕服務。

SpringBoot進階教程(六十七)RateLimiter限流

圖片來源於網絡,侵刪。

可以看出,漏桶算法可以很好的控制流量的訪問速度,一旦超過該速度就拒絕服務。

2.2 令牌桶算法

令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發送到網絡上的數據的數目,並允許突發數據的發送。

令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。從原理上看,令牌桶算法和漏桶算法是相反的,一個“進水”,一個是“漏水”。

對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。

如下圖所示,令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。

SpringBoot進階教程(六十七)RateLimiter限流

圖片來源於網絡,侵刪。

本文後面實例中講到的RateLimiter(Google的Guava包)正是使用的令牌桶算法。

2.3 漏桶算法和令牌桶算法的區別

漏桶算法的出水速度是恆定的,那麼意味着如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢出)。漏桶算法通常可以用於限制訪問外部接口的流量,保護其他人系統,比如我們請求銀行接口,通常要限制併發數。

令牌桶算法生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該算法可以在短時間內請求拿到大量令牌,可以處理瞬時流量,而且拿令牌的過程並不是消耗很大的事情。令牌桶算法通常可以用於限制被訪問的流量,保護自身系統。

需要注意的是,在某些情況下,漏桶算法不能夠有效地使用網絡資源,因爲漏桶的漏出速率是固定的,所以即使網絡中沒有發生擁塞,漏桶算法也不能使某一個單獨的數據流達到端口速率。因此,漏桶算法對於存在突發特性的流量來說缺乏效率。而令牌桶算法則能夠滿足這些具有突發特性的流量。通常,漏桶算法與令牌桶算法結合起來爲網絡流量提供更高效的控制。

vRateLimiter

3.1 基礎介紹

RateLimiter 從概念上來講,速率限制器會在可配置的速率下分配許可證。如果必要的話,每個acquire() 會阻塞當前線程直到許可證可用後獲取該許可證。一旦獲取到許可證,不需要再釋放許可證。

RateLimiter使用的是一種叫令牌桶的流控算法,RateLimiter會按照一定的頻率往桶裏扔令牌,線程拿到令牌才能執行,比如你希望自己的應用程序QPS不要超過1000,那麼RateLimiter設置1000的速率後,就會每秒往桶裏扔1000個令牌。

3.2 方法摘要

修飾符和類型方法和描述
double acquire()
從RateLimiter獲取一個許可,該方法會被阻塞直到獲取到請求
double acquire(int permits)
從RateLimiter獲取指定許可數,該方法會被阻塞直到獲取到請求
static RateLimiter create(double permitsPerSecond)
根據指定的穩定吞吐率創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少查詢)
static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
根據指定的穩定吞吐率和預熱期來創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少個請求量),在這段預熱時間內,RateLimiter每秒分配的許可數會平穩地增長直到預熱期結束時達到其最大速率。(只要存在足夠請求數來使其飽和)
double getRate()
返回RateLimiter 配置中的穩定速率,該速率單位是每秒多少許可數
void setRate(double permitsPerSecond)
更新RateLimite的穩定速率,參數permitsPerSecond 由構造RateLimiter的工廠方法提供。
String toString()
返回對象的字符表現形式
boolean tryAcquire()
從RateLimiter 獲取許可,如果該許可可以在無延遲下的情況下立即獲取得到的話
boolean tryAcquire(int permits)
從RateLimiter 獲取許可數,如果該許可數可以在無延遲下的情況下立即獲取得到的話
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
從RateLimiter 獲取指定許可數如果該許可數可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可數的話,那麼立即返回false (無需等待)
boolean tryAcquire(long timeout, TimeUnit unit)
從RateLimiter 獲取許可如果該許可可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可的話,那麼立即返回false(無需等待)

3.3 實驗

3.3.1 添加引用

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>20.0</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.10</version>
        </dependency>

3.3.2 添加註解

package learn.web.interceptor;

import java.lang.annotation.*;

/**
 * @author toutou
 * @date by 2020/12
 * @des 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limiting {

    // 默認每秒放入桶中的token
    double limitNum() default 20;

    String name() default "";
}

3.3.3 aop切面

package learn.web.interceptor;

import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author toutou
 * @date by 2020/12
 * @des
 */
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    private ConcurrentHashMap<String, RateLimiter> RATE_LIMITER  = new ConcurrentHashMap<>();
    private RateLimiter rateLimiter;

    @Pointcut("@annotation(learn.web.interceptor.Limiting)")
    public void serviceLimit() {
    }

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //獲取攔截的方法名
        Signature sig = point.getSignature();
        //獲取攔截的方法名
        MethodSignature msig = (MethodSignature) sig;
        //返回被織入增加處理目標對象
        Object target = point.getTarget();
        //爲了獲取註解信息
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        //獲取註解信息
        Limiting annotation = currentMethod.getAnnotation(Limiting.class);
        double limitNum = annotation.limitNum(); //獲取註解每秒加入桶中的token
        String functionName = msig.getName(); // 註解所在方法名區分不同的限流策略

        if(RATE_LIMITER.containsKey(functionName)){
            rateLimiter=RATE_LIMITER.get(functionName);
        }else {
            RATE_LIMITER.put(functionName, RateLimiter.create(limitNum));
            rateLimiter=RATE_LIMITER.get(functionName);
        }
        if(rateLimiter.tryAcquire()) {
            log.info("處理完成");
            return point.proceed();
        } else {
            throw new RuntimeException("服務器繁忙,請稍後再試。");
        }
    }
}

3.3.4 添加Controller測試

package learn.web.controller;

import learn.model.vo.Result;
import learn.web.interceptor.Limiting;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author toutou
 * @date by 2020/12
 * @des
 */
@Slf4j
@RestController
public class IndexController {
    /**
     * 使用限流注解的接口1
     * @return
     */
    @GetMapping("/limit1")
    @Limiting(limitNum = 1, name = "limiting1")
    public Result Limit1() {
        return Result.setSuccessResult("limiting1");
    }

    /**
     * 使用限流注解的接口2
     * @return
     */
    @GetMapping("/limit2")
    @Limiting(limitNum = 5, name = "limiting2")
    public Result Limit2() {
        return Result.setSuccessResult("limiting2");
    }
}

3.3.5 測試效果

ab測試截圖

SpringBoot進階教程(六十七)RateLimiter限流

瀏覽器截圖

SpringBoot進階教程(六十七)RateLimiter限流

其他參考/學習資料:

v源碼地址

https://github.com/toutouge/javademosecond/tree/master/hellolearn


作  者:請叫我頭頭哥
出  處:http://www.cnblogs.com/toutou/
關於作者:專注於基礎平臺的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回覆。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角推薦一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!

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