【高併發】億級流量場景下如何爲HTTP接口限流?看完我懂了!!

寫在前面

在互聯網應用中,高併發系統會面臨一個重大的挑戰,那就是大量流高併發訪問,好比:天貓的雙11、京東61八、秒殺、搶購促銷等,這些都是典型的大流量高併發場景。關於秒殺,小夥伴們能夠參見個人另外一篇文章《【高併發】高併發秒殺系統架構解密,不是全部的秒殺都是秒殺!html

關於【冰河技術】微信公衆號,解鎖更多【高併發】專題文章。java

注意:因爲原文篇幅比較長,因此被拆分爲:理論、算法、實戰(HTTP接口實戰+分佈式限流實戰)三大部分。git

理論篇:《【高併發】如何實現億級流量下的分佈式限流?這些理論你必須掌握!!github

算法篇:《【高併發】如何實現億級流量下的分佈式限流?這些算法你必須掌握!!web

項目源碼已提交到github:https://github.com/sunshinelyz/mykit-ratelimiter面試

HTTP接口限流實戰

這裏,咱們實現Web接口限流,具體方式爲:使用自定義註解封裝基於令牌桶限流算法實現接口限流。redis

不使用註解實現接口限流

搭建項目

這裏,咱們使用SpringBoot項目來搭建Http接口限流項目,SpringBoot項目本質上仍是一個Maven項目。因此,小夥伴們能夠直接建立一個Maven項目,我這裏的項目名稱爲mykit-ratelimiter-test。接下來,在pom.xml文件中添加以下依賴使項目構建爲一個SpringBoot項目。算法

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>io.mykit.limiter</groupId>
    <artifactId>mykit-ratelimiter-test</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>mykit-ratelimiter-test</name>

    <properties>
        <guava.version>28.2-jre</guava.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version><!--$NO-MVN-MAN-VER$-->
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

能夠看到,我在項目中除了引用了SpringBoot相關的Jar包外,還引用了guava框架,版本爲28.2-jre。spring

建立核心類

這裏,我主要是模擬一個支付接口的限流場景。首先,咱們定義一個PayService接口和MessageService接口。PayService接口主要用於模擬後續的支付業務,MessageService接口模擬發送消息。接口的定義分別以下所示。apache

  • PayService
package io.mykit.limiter.service;
import java.math.BigDecimal;
/**
 * @author binghe
 * @version 1.0.0
 * @description 模擬支付
 */
public interface PayService {
    int pay(BigDecimal price);
}
  • MessageService
package io.mykit.limiter.service;
/**
 * @author binghe
 * @version 1.0.0
 * @description 模擬發送消息服務
 */
public interface MessageService {
    boolean sendMessage(String message);
}

接下來,建立兩者的實現類,分別以下。

  • MessageServiceImpl
package io.mykit.limiter.service.impl;
import io.mykit.limiter.service.MessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
 * @author binghe
 * @version 1.0.0
 * @description 模擬實現發送消息
 */
@Service
public class MessageServiceImpl implements MessageService {
    private final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
    @Override
    public boolean sendMessage(String message) {
        logger.info("發送消息成功===>>" + message);
        return true;
    }
}
  • PayServiceImpl
package io.mykit.limiter.service.impl;
import io.mykit.limiter.service.PayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
 * @author binghe
 * @version 1.0.0
 * @description 模擬支付
 */
@Service
public class PayServiceImpl implements PayService {
    private final Logger logger = LoggerFactory.getLogger(PayServiceImpl.class);
    @Override
    public int pay(BigDecimal price) {
        logger.info("支付成功===>>" + price);
        return 1;
    }
}

因爲是模擬支付和發送消息,因此,我在具體實現的方法中打印出了相關的日誌,並無實現具體的業務邏輯。

接下來,就是建立咱們的Controller類PayController,在PayController類的接口pay()方法中使用了限流,每秒鐘向桶中放入2個令牌,而且客戶端從桶中獲取令牌,若是在500毫秒內沒有獲取到令牌的話,咱們能夠則直接走服務降級處理。

PayController的代碼以下所示。

package io.mykit.limiter.controller;
import com.google.common.util.concurrent.RateLimiter;
import io.mykit.limiter.service.MessageService;
import io.mykit.limiter.service.PayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試接口限流
 */
@RestController
public class PayController {
    private final Logger logger = LoggerFactory.getLogger(PayController.class);
    /**
     * RateLimiter的create()方法中傳入一個參數,表示以固定的速率2r/s,即以每秒2個令牌的速率向桶中放入令牌
     */
    private RateLimiter rateLimiter = RateLimiter.create(2);

    @Autowired
    private MessageService messageService;
    @Autowired
    private PayService payService;
    @RequestMapping("/boot/pay")
    public String pay(){
        //記錄返回接口
        String result = "";
        //限流處理,客戶端請求從桶中獲取令牌,若是在500毫秒沒有獲取到令牌,則直接走服務降級處理
        boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
        if (!tryAcquire){
            result = "請求過多,降級處理";
            logger.info(result);
            return result;
        }
        int ret = payService.pay(BigDecimal.valueOf(100.0));
        if(ret > 0){
            result = "支付成功";
            return result;
        }
        result = "支付失敗,再試一次吧...";
        return result;
    }
}

最後,咱們來建立mykit-ratelimiter-test項目的核心啓動類,以下所示。

package io.mykit.limiter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author binghe
 * @version 1.0.0
 * @description 項目啓動類
 */
@SpringBootApplication
public class MykitLimiterApplication {

    public static void main(String[] args){
        SpringApplication.run(MykitLimiterApplication.class, args);
    }
}

至此,咱們不使用註解方式實現限流的Web應用就基本完成了。

運行項目

項目建立完成後,咱們來運行項目,運行SpringBoot項目比較簡單,直接運行MykitLimiterApplication類的main()方法便可。

項目運行成功後,咱們在瀏覽器地址欄輸入連接:http://localhost:8080/boot/pay。頁面會輸出「支付成功」的字樣,說明項目搭建成功了。以下所示。

此時,我只訪問了一次,並無觸發限流。接下來,咱們不停的刷瀏覽器,此時,瀏覽器會輸出「支付失敗,再試一次吧...」的字樣,以下所示。

在PayController類中還有一個sendMessage()方法,模擬的是發送消息的接口,一樣使用了限流操做,具體代碼以下所示。

@RequestMapping("/boot/send/message")
public String sendMessage(){
    //記錄返回接口
    String result = "";
    //限流處理,客戶端請求從桶中獲取令牌,若是在500毫秒沒有獲取到令牌,則直接走服務降級處理
    boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
    if (!tryAcquire){
        result = "請求過多,降級處理";
        logger.info(result);
        return result;
    }
    boolean flag = messageService.sendMessage("恭喜您成長值+1");
    if (flag){
        result = "消息發送成功";
        return result;
    }
    result = "消息發送失敗,再試一次吧...";
    return result;
}

sendMessage()方法的代碼邏輯和運行效果與pay()方法相同,我就再也不瀏覽器訪問 http://localhost:8080/boot/send/message 地址的訪問效果了,小夥伴們能夠自行驗證。

不使用註解實現限流缺點

經過對項目的編寫,咱們能夠發現,當在項目中對接口進行限流時,不使用註解進行開發,會致使代碼出現大量冗餘,每一個方法中幾乎都要寫一段相同的限流邏輯,代碼十分冗餘。

如何解決代碼冗餘的問題呢?咱們可使用自定義註解進行實現。

使用註解實現接口限流

使用自定義註解,咱們能夠將一些通用的業務邏輯封裝到註解的切面中,在須要添加註解業務邏輯的方法上加上相應的註解便可。針對咱們這個限流的實例來講,能夠基於自定義註解實現。

實現自定義註解

實現,咱們來建立一個自定義註解,以下所示。

package io.mykit.limiter.annotation;
import java.lang.annotation.*;
/**
 * @author binghe
 * @version 1.0.0
 * @description 實現限流的自定義註解
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRateLimiter {
    //向令牌桶放入令牌的速率
    double rate();
    //從令牌桶獲取令牌的超時時間
    long timeout() default 0;
}

自定義註解切面實現

接下來,咱們還要實現一個切面類MyRateLimiterAspect,以下所示。

package io.mykit.limiter.aspect;

import com.google.common.util.concurrent.RateLimiter;
import io.mykit.limiter.annotation.MyRateLimiter;
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.stereotype.Component;

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

/**
 * @author binghe
 * @version 1.0.0
 * @description 通常限流切面類
 */
@Aspect
@Component
public class MyRateLimiterAspect {

    private RateLimiter rateLimiter = RateLimiter.create(2);

    @Pointcut("execution(public * io.mykit.limiter.controller.*.*(..))")
    public void pointcut(){

    }

    /**
     * 核心切面方法
     */
    @Around("pointcut()")
    public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();

        //使用反射獲取方法上是否存在@MyRateLimiter註解
        MyRateLimiter myRateLimiter = signature.getMethod().getDeclaredAnnotation(MyRateLimiter.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){
            //服務降級
            fullback();
            return null;
        }
        //獲取到令牌,直接執行
        return proceedingJoinPoint.proceed();

    }

    /**
     * 降級處理
     */
    private void fullback() {
        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();
            }
        }
    }
}

自定義切面的功能比較簡單,我就不細說了,你們有啥問題能夠關注【冰河技術】微信公衆號來進行提問。

接下來,咱們改造下PayController類中的sendMessage()方法,修改後的方法片斷代碼以下所示。

@MyRateLimiter(rate = 1.0, timeout = 500)
@RequestMapping("/boot/send/message")
public String sendMessage(){
    //記錄返回接口
    String result = "";
    boolean flag = messageService.sendMessage("恭喜您成長值+1");
    if (flag){
        result = "消息發送成功";
        return result;
    }
    result = "消息發送失敗,再試一次吧...";
    return result;
}

運行部署項目

部署項目比較簡單,只須要運行MykitLimiterApplication類下的main()方法便可。這裏,爲了簡單,咱們仍是從瀏覽器中直接輸入連接地址來進行訪問

效果以下所示。

接下來,咱們不斷的刷新瀏覽器。會出現「消息發送失敗,再試一次吧..」的字樣,說明已經觸發限流操做。

基於限流算法實現限流的缺點

上面介紹的限流方式都只能用於單機部署的環境中,若是將應用部署到多臺服務器進行分佈式、集羣,則上面限流的方式就不適用了,此時,咱們須要使用分佈式限流。至於在分佈式場景下,如何實現限流操做,咱們就在下一篇中進行介紹。

重磅福利

關注「 冰河技術 」微信公衆號,後臺回覆 「設計模式」 關鍵字領取《深刻淺出Java 23種設計模式》PDF文檔。回覆「Java8」關鍵字領取《Java8新特性教程》PDF文檔。兩本PDF均是由冰河原創並整理的超硬核教程,面試必備!!

好了,今天就聊到這兒吧!別忘了點個贊,給個在看和轉發,讓更多的人看到,一塊兒學習,一塊兒進步!!

寫在最後

若是你以爲冰河寫的還不錯,請微信搜索並關注「 冰河技術 」微信公衆號,跟冰河學習高併發、分佈式、微服務、大數據、互聯網和雲原生技術,「 冰河技術 」微信公衆號更新了大量技術專題,每一篇技術文章乾貨滿滿!很多讀者已經經過閱讀「 冰河技術 」微信公衆號文章,吊打面試官,成功跳槽到大廠;也有很多讀者實現了技術上的飛躍,成爲公司的技術骨幹!若是你也想像他們同樣提高本身的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公衆號吧,天天更新超硬核技術乾貨,讓你對如何提高技術能力再也不迷茫!

相關文章
相關標籤/搜索