基于Guava RateLimiter,實(shí)現(xiàn)一個(gè)“API級(jí)別的限流”注解

首先,解釋一下標(biāo)題。
我們有一個(gè)需求:能為每個(gè)接口單獨(dú)設(shè)置一個(gè)限流值。那么每個(gè)接口都需要增加相應(yīng)的代碼,只有自己寫一個(gè)注解,使用成本才低,對(duì)業(yè)務(wù)代碼的侵入也低。


一、整體思路
  1. 自定義一個(gè)注解,里面有個(gè)限流值的變量;
  2. 在需要的接口上,加上該注解,并設(shè)置好限流值,比如:@RateLimit(5);
  3. 寫一個(gè)針對(duì)該注解的切面,before()階段進(jìn)行限流判斷和限流處理。
二、開始編寫代碼
  1. 自定義注解
@Inherited
@Documented
@Target({ElementType.METHOD}) //方法級(jí)別
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
     /**
     * 為了方便管理各個(gè)接口的限流值,這里是設(shè)置對(duì)應(yīng)application.properties里的屬性key,key對(duì)應(yīng)的value才是限流值
     */
    String limitKey() default "api.default.limit";
}
  1. 每個(gè)API上使用該注解
@ApiOperation(value = "查詢產(chǎn)品詳情")
@RequestMapping(value = "/{productId}", method = RequestMethod.GET)
@RateLimit(limitKey = "xxxxx.xxxx")
public Result<Product> queryProduct(@PathVariable("productId") Integer productId) {
       return new Result<>(xxClient.queryProduct(productId));
}
  1. 針對(duì)該注解的切面(限流的核心)
@Component
@Aspect
@Slf4j
public class RateLimitAspect {
    /**
     * 針對(duì)含有該注解的地方進(jìn)行切入
     */
    @Pointcut("@annotation(com.xxx.xxx.RateLimit)")
    public void serviceLimit() {
    }
    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            //限流判斷,和限流處理
    }

切面的大體框架有了,我們現(xiàn)在就剩具體的限流邏輯代碼

三、Guava RateLimiter

限流有很多算法,包括令牌桶、漏桶。根據(jù)需求,我們選擇的是令牌桶。

令牌桶:系統(tǒng)會(huì)以一個(gè)恒定的速度往桶里放入令牌,而如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒有令牌可取時(shí),則拒絕服務(wù)。

而令牌桶的具體實(shí)現(xiàn),如果我們自己完成,就有些復(fù)雜了,所以這里我選擇了Guava依賴包里的RateLimiter。先看代碼吧

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>20.0</version>
        </dependency>
@Autowired
    private Environment env;

    //用來存放不同接口的RateLimiter(key為接口名稱,value為RateLimiter)
    private ConcurrentHashMap<String, RateLimiter> rateLimitMap = new ConcurrentHashMap<>();

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object obj = null;
        //獲取攔截的簽名
        Signature sig = joinPoint.getSignature();
        //獲取攔截的方法名
        MethodSignature msig = (MethodSignature) sig;
        //返回被織入增加處理目標(biāo)對(duì)象
        Object target = joinPoint.getTarget();
        //為了獲取注解信息
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        //獲取注解信息
        RateLimit annotation = currentMethod.getAnnotation(RateLimit.class);
        //在application.properties中獲取注解每秒加入桶中的token
        String limitNum = env.getProperty(annotation.limitKey());
        // 注解所在方法名區(qū)分不同的限流策略
        String functionName = msig.toShortString();
        //如果值為0,則為不進(jìn)行限流
        if (limitNum == null || limitNum.equals("0")) {
            obj = joinPoint.proceed();
        } else {
            //獲取rateLimiter
            if(!rateLimitMap.containsKey(functionName)){
                //使用最簡(jiǎn)潔的方法來創(chuàng)建RateLimiter,RateLimiter.create(double xx),如果有需要,可自行設(shè)置RateLimiter其他屬性
                rateLimitMap.put(functionName, RateLimiter.create(Double.parseDouble(limitNum)));
            }
            RateLimiter rateLimiter = rateLimitMap.get(functionName);
                        //嘗試獲得一個(gè)令牌
            if (rateLimiter.tryAcquire(1)) {
                //執(zhí)行方法
                obj = joinPoint.proceed();
            } else {
                //拒絕了請(qǐng)求(服務(wù)降級(jí)),這是自己定義的異常類
                throw new CommonException("拒絕了訪問open api方法的請(qǐng)求", FieldCodeEnum.OPEN_API, null, DetailCodeEnum.ACCESS, ErrorCodeEnum.OUT_OF_LIMIT);
            }
        }
        return obj;
    }
api.default.limit=5
四、完成

可以使用PostMan軟件發(fā)起壓力測(cè)試請(qǐng)求,比如1秒鐘發(fā)出去10個(gè),結(jié)果會(huì)是6個(gè)通行了,但后4個(gè)被拒絕了。
明明我們?cè)O(shè)置的限流值是5,為何6個(gè)通行了呢?見官方描述:

When the incoming request rate exceeds {@code permitsPerSecond} the
rate limiter will release one permit every {@code (1.0 / permitsPerSecond)} seconds.

這就是 RateLimiter 的預(yù)加載功能。


以上只是我搭建了一個(gè)限流的基礎(chǔ)框架,大家可以繼續(xù)完善它,以實(shí)現(xiàn)自己的需要。
歡迎大家一起交流相關(guān)知識(shí)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容