基于 Hystrix 高并发服务限流第 6 篇 —— 服务限流,基于 RateLimiter 实现

查看之前的博客可以点击顶部的【分类专栏】

 

什么是服务限流?

服务限流就是对接口访问进行限制,常用服务限流算法令牌桶、漏桶。计数器也可以进行粗暴限流实现。更多详情可以查看博客:https://blog.csdn.net/BiandanLoveyou/article/details/117376931

 

本篇博客主要讲解令牌桶算法的限流。

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。

 

使用 RateLimiter 实现令牌桶限流

RateLimiter是 guava (Google)提供的基于令牌桶算法的实现类,可以非常简单的完成接口限流,并且根据系统的实际情况来调整生成token的速率。通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。

 

我们创建一个 limit-server 项目

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.study</groupId>
    <artifactId>limit-server</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <dependencies>
        <!--Spring boot 集成包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.0-jre</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
</project>

我们创建一个 controller,代码如下:

package com.study.controller;

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-21 下午 12:59
 */
@RestController
public class LimitController {

    //表示每秒钟生成1个令牌存入令牌桶中
    RateLimiter rateLimiter = RateLimiter.create(1.0);

    //模拟秒杀下单场景
    @GetMapping("/order")
    public String order() {
        String result = "";
        //限制在 500 毫秒内获取令牌,无法获取返回 false
        boolean flag = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);

        if (flag) {
            result = "恭喜您,抢到了一个惊喜!";
            //然后实现业务逻辑
        } else {
            result = "没抢到,等会再试试!";
        }
        System.out.println(result);
        return result;
    }

}

bootstrap.yml

server:
  port: 80

spring:
  application:
    name: limit-server

# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
  client:
    fetch-registry: false
    register-with-eureka: false

 

编写启动类。启动测试:http://127.0.0.1/order

控制台输出:

 

然后不断的刷新前端页面,测试:

 

这就是令牌桶算法的结果。

 

封装 RateLimiter 令牌桶算法

OK,我们尝试封装一下令牌桶算法,只需要在方法里添加一个注解即可。然后令牌桶的存入桶的速率可以配置。我们的令牌桶是针对某一个接口而制定的,也就是说添加了我们自定义的注解之后,就为该接口开辟了令牌桶算法。

我们用到 AOP,因此在 pom.xml 增加依赖:

        <!-- springboot-aop 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

 

步骤1:自定义注解

package com.study.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-21 下午 3:18
 */
@Target(value = ElementType.METHOD)//表示此注解用在方法上
@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
public @interface ExtRateLimiter {

    /**
     * 每秒添加的数量
     * @return
     */
    double value();

    /**
     * 限制等待时间
     * @return
     */
    long limitTime();
}

 

步骤2:编写 AOP 切面

package com.study.aop;

import com.google.common.util.concurrent.RateLimiter;
import com.study.annotation.ExtRateLimiter;
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.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

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

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-21 下午 3:24
 */
@Aspect
@Component
public class RateLimiterAOP {

    //某个接口对应的令牌桶
    private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    //定义切面,拦截 controller 层
    @Pointcut("execution(public * com.study.controller.*.*(..))")
    public void myPointCut() {

    }

    //环绕通知(目前主要用于抢单的方法上)
    @Around("myPointCut()")
    public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
        //判断方法上是否有注解 @ExistsApiToken
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        ExtRateLimiter extRateLimiter = methodSignature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class);
        //如果不包含该注解,正常往下执行即可
        if (extRateLimiter != null) {
            //获取注解上的参数,配置固定速率
            double value = extRateLimiter.value();
            long limitTime = extRateLimiter.limitTime();

            RateLimiter rateLimiter = getRateLimiter(value);
            //判断令牌桶获取 token 是否超时
            boolean acquire = rateLimiter.tryAcquire(limitTime, TimeUnit.MILLISECONDS);
            //如果超时
            if (!acquire) {
                serviceFallBack();
                return null;
            }
        }
        return joinPoint.proceed();
    }

    //获取 RateLimiter 对象
    private RateLimiter getRateLimiter(double value) {
        //获取当前URL
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String requestURI = request.getRequestURI();
        RateLimiter rateLimiter = null;
        if (!rateLimiterMap.containsKey(requestURI)) {
            //开启令牌限流
            rateLimiter = RateLimiter.create(value);//独立线程
            rateLimiterMap.put(requestURI, rateLimiter);
        } else {
            rateLimiter = rateLimiterMap.get(requestURI);
        }
        return rateLimiter;
    }

    //超时,服务降级
    private void serviceFallBack() throws IOException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = attributes.getResponse();
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        String msg = "目前活动太火爆,抢购人数较多,您未能抢到,请稍后重试~";
        System.out.println(msg);
        try {
            writer.println(msg);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }

}

 

然后 controller  的代码就简洁了,如下:

package com.study.controller;

import com.study.annotation.ExtRateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-21 下午 12:59
 */
@RestController
public class LimitController {

    //模拟秒杀下单场景
    @GetMapping("/order")
    @ExtRateLimiter(value = 2, limitTime = 500)
    public String order() {
        String result = "恭喜您,抢到了一个惊喜!";
        System.out.println(result);
        return result;
    }

}

重启,测试:

 

OK,关于高并发服务限流讲解到这。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值