1. 程式人生 > 實用技巧 >記介面限流令牌桶的學習

記介面限流令牌桶的學習

在高併發系統中,存在著巨大的挑戰,大流量高併發的訪問。一些常見的有天貓的雙十一、京東618、秒殺以及延時促銷等。短時間內的如此巨大的訪問流量往往會給資料庫造成巨大的壓力,進而影響伺服器端的穩定性,那麼我們的解決方案包括有:

  1. 前端用nginx做負載均衡;
  2. 對伺服器端訪問頻率較多的查詢介面做redis快取,減小資料庫的壓力;
  3. 限流

  今天我自己就來學習一下關於如何做到限流。起初我想的是對請求介面做切面處理,獲取傳送請求端的IP,然後將IP、請求的介面路徑、時間等資訊存入redis,然後對每一次的請求和redis中存入的資訊進行比對,如果IP相同且兩次間隔時間很小,那麼我們就可以直接返回結果而不進入到controller層,當然也可以存入mysql資料庫中,兩者區別是,mysql中是持久化的,而redis中會因為伺服器掛掉等原因造成資料丟失,當然其實影響也不大,因為限流主要就要保證某一時間點內對訪問請求進行限制,所以相關資訊存入redis的儲存時間不用過長,這些都可以根據實際情況來予以處理。

  然而在部落格園中瀏覽了一篇文章,瞭解到令牌桶,所以就自己實現一下用令牌桶來限流。關於令牌桶的一些知識,有興趣的可以去了解一下。

  首先,構建springboot專案,我使用的idea2018 +jdk1.8。專案檔案目錄如圖所示:

接著就是controller層、service層、dao層的程式碼編寫,

  

import com.white.ratelimiter.service.MassageService;
import com.white.ratelimiter.service.PayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.concurrent.TimeUnit;
/** * 功能模組() * * @Author white Liu * @Date 2020/7/31 10:10 * @Version 1.0 */ @RestController @RequestMapping("/api") @Slf4j public class PayController { @Autowired PayService payService; @Autowired MassageService massageService; /** * RateLimiter.create(2)表示以固定速率2r/s,即以每秒2個令牌的速率放至到令牌桶中 */ private RateLimiter rateLimiter = RateLimiter.create(1); @GetMapping("/pay/get") public BaseResult pay(){ //限流處理,客戶端請求從桶中獲取令牌,如果在500毫秒沒有獲取到令牌,則直接走服務降級處理 boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS); if(!tryAcquire){ log.info("請求過多,請稍後"); return BaseResult.error(500,"當前請求過多!"); } int ref = payService.doPay(BigDecimal.valueOf(99.8)); if(ref>0){ log.info("支付成功"); return BaseResult.ok().put("msg","支付成功"); } return BaseResult.error(400,"支付失敗,請重新嘗試"); } }

啟動程式後,用瀏覽器請求介面,然後再不斷快速重新整理,頁面會顯示如下圖所示,可見令牌桶限流能夠生效!

下面第二個介面嘗試優化程式碼量,使用自定義註解以及切面類來實現令牌桶限流。

  首先自定義註解:

package com.white.ratelimiter.annotation;

/**
 * 功能模組(令牌桶限流注解)
 *
 * @Author white Liu
 * @Date 2020/8/1 11:55
 * @Version 1.0
 */

import java.lang.annotation.*;

@Documented
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLimiter {
    //向令牌桶放入令牌的速率
    double rate();
    //從令牌桶獲取令牌的超時時間
    long timeout() default 0;
}

接著編寫切面類,獲取使用該自定義註解的介面,然後使用令牌桶來對該介面進行限流

package com.white.ratelimiter.aspect;

import com.google.common.util.concurrent.RateLimiter;
import com.white.ratelimiter.annotation.MyLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

/**
 * 功能模組()
 *
 * @Author white Liu
 * @Date 2020/8/1 11:59
 * @Version 1.0
 */
@Component
@Aspect
@Slf4j
@Scope
public class MyRateLimiterAspect {
    @Autowired
    private HttpServletResponse response;//注入HttpServletResponse物件,進行降級處理,返回必要資訊提示使用者
    
    private RateLimiter rateLimiter = RateLimiter.create(1);

    @Pointcut("execution(public * com.white.ratelimiter.controller.*.*(..))")
    public void cutPoint(){}
    @Around("cutPoint()")
    public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();

        //使用反射獲取方法上是否存在@MyRateLimiter註解
        MyLimiter myRateLimiter = signature.getMethod().getDeclaredAnnotation(MyLimiter.class);
        if(myRateLimiter == null){
            //程式正常執行,執行目標方法
            return proceedingJoinPoint.proceed();
        }
        //獲取註解上的引數
        //獲取配置的速率
        double rate = myRateLimiter.rate();
        //獲取客戶端等待令牌的時間
        long timeout = myRateLimiter.timeout();

        //設定限流速率
        rateLimiter.setRate(rate);

        //判斷客戶端獲取令牌是否超時
        boolean tryAcquire = rateLimiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);
        if(!tryAcquire){
            log.info("當前訪問太多,請稍後嘗試");
            //服務降級
            callback();
            return null;
        }
        //獲取到令牌,直接執行
        return proceedingJoinPoint.proceed();
    }
    /**
     * 降級處理
     */
    private void callback() {
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter writer = null;
        try {
            writer =  response.getWriter();
            writer.println("出錯了,請重新嘗試");
            writer.flush();;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(writer != null){
                writer.close();
            }
        }
    }
}

然後在介面處新增自定義註解

    @GetMapping("/msg/get")
    @MyLimiter(rate = 1.0,timeout = 500)
    public BaseResult sendMsg(){
        boolean flag = massageService.sendMsg("恭喜您成長值+1");
        if (flag){
            log.info( "訊息傳送成功");
            return BaseResult.ok("訊息傳送成功");
        }
        return BaseResult.error(400,"訊息傳送失敗,請重新嘗試一次吧...");
    }

至此,自定義註解限流完成,啟動程式,開啟瀏覽器,輸入rul,得到下圖所示

嘗試不斷快速重新整理頁面,自定義註解會生效,頁面會顯示

github程式碼下載地址:點選這裡