jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例

一、前言

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

1缓存 缓存的目的是提升系统访问速度和增大系统处理容量
2降级 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
3限流 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处

涉及到的技术:

(1)SpringBoot拦截器

(2)ScheduledThreadPoolExecutor定时任务

(3)Redis队列

(4)SpringBoot RedisTemplate操作redis

(5)jmeter测试工具

使用令牌桶算法实现接口限流

 

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第1张图片

令牌桶算法(Token Bucket):随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token,如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。

在SpringBoot项目中通过拦截器拦截增加了@LimitingRequired注解的接口查询是否有token来决定是否限制请求。

二、关键代码

@LimitingRequired注解

package com.asyf.demo.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LimitingRequired {
    boolean required() default true;
}

常量类

package com.asyf.demo.common;

public class Constant {

    public static final String TOKEN_BUCKET_KEY = "token_bucket";//令牌桶key

    public static final Long TOKEN_BUCKET_SIZE = 10L;//令牌桶容量

}

类ApplicationRunnerImpl,此方法实现了向桶中添加令牌的功能

package com.asyf.demo.config;

import com.asyf.demo.common.Constant;
import com.asyf.demo.utils.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component
@ConfigurationProperties(
        prefix = "server",
        ignoreUnknownFields = true
)
public class ApplicationRunnerImpl implements ApplicationRunner {

    private Integer port;

    @Autowired
    private RedisTemplate redisTemplate;

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("通过实现ApplicationRunner接口,在spring boot项目启动后打印参数");
        String[] sourceArgs = args.getSourceArgs();
        for (String arg : sourceArgs) {
            System.out.print(arg + " ");
        }
        //给令牌桶添加令牌-分布式部署时只需要一个端口添加令牌桶即可
        if (port.compareTo(8080) == 0) {
            addTokenBucket();
        }
    }

    private void addTokenBucket() {
        ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(1);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                ListOperations listOperations = redisTemplate.opsForList();
                //获取队列的长度,当redis队列不存在返回0
                Long size = listOperations.size(Constant.TOKEN_BUCKET_KEY);
                if (size.compareTo(Constant.TOKEN_BUCKET_SIZE) == -1) {
                    long currentTimeMillis = System.currentTimeMillis();
                    System.out.println("添加令牌---线程:" + Thread.currentThread().getName() + ",执行时间:" + currentTimeMillis + "-" + port);
                    listOperations.leftPush(Constant.TOKEN_BUCKET_KEY, DateUtil.dateToString(new Date(currentTimeMillis)));
                }
            }
        };
        //定时器间隔设置成100毫秒,每100ms添加一个令牌,即限制了每秒钟请求次数最多有10个请求+预存的10的令牌(如果有突发流量,每秒钟最多请求次数接近10)
        scheduled.scheduleAtFixedRate(runnable, 5, 100, TimeUnit.MILLISECONDS);
    }

}

拦截器判断是否限流,通过队列数量(即令牌数量)控制是否限流

package com.asyf.demo.intercepter;

import com.alibaba.fastjson.JSONObject;
import com.asyf.demo.annotation.LimitingRequired;
import com.asyf.demo.common.Constant;
import com.asyf.demo.entity.ResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;

@Component
public class TokenBucketIntercepter extends HandlerInterceptorAdapter {

    private static final Logger logger = LoggerFactory.getLogger(TokenBucketIntercepter.class);

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        LimitingRequired annotation = method.getAnnotation(LimitingRequired.class);
        //判断是否需要限流
        if (annotation != null && annotation.required()) {
            boolean isAllowAccess = isAllowAccess();
            if (!isAllowAccess) {
                returnJson(response, "当前访问人数过多,请稍后重试!");
                return false;
            }
        }
        return true;
    }

    private boolean isAllowAccess() {
        ListOperations listOperations = redisTemplate.opsForList();
        Long size = listOperations.size(Constant.TOKEN_BUCKET_KEY);
        //令牌桶中没有令牌,返回false,这样就起到了限流的作用
        logger.info("令牌数量:" + size);
        if (size < 1) {
            logger.error("请求-----------502");
            return false;
        }
        //认定当前请求有效,取出令牌桶
        listOperations.rightPop(Constant.TOKEN_BUCKET_KEY);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //logger.info("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("afterCompletion-访问路径:" + request.getContextPath() + request.getServletPath());
    }

    private void returnJson(HttpServletResponse response, String msg) {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpStatus.BAD_GATEWAY.value());
        try {
            writer = response.getWriter();
            ResponseEntity responseEntity = new ResponseEntity<>(HttpStatus.BAD_GATEWAY.value(), msg, msg);
            Object o = JSONObject.toJSON(responseEntity);
            writer.print(o);
            writer.flush();
        } catch (IOException e) {
            logger.error("拦截器输出流异常" + e.toString());
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

Controller接口,添加注解控制接口需要限流

  @RequestMapping(value = "test")
    @LimitingRequired(required = true)
    public ResponseEntity test() {
        ListOperations listOperations = redisTemplate.opsForList();
        return new ResponseEntity<>("test:队列长度:" + listOperations.size(Constant.TOKEN_BUCKET_KEY));
    }

ResponseEntity返回值实体类 

package com.asyf.demo.entity;

public class ResponseEntity {

    private int code;

    private String message;

    private T result;

    public ResponseEntity(int code, String message, T result) {
        this.code = code;
        this.message = message;
        this.result = result;
    }

    public ResponseEntity(String message, T result) {
        this.code = 200;
        this.message = message;
        this.result = result;
    }

    public ResponseEntity(T result) {
        this.code = 200;
        this.message = "Success";
        this.result = result;
    }

    public ResponseEntity() {
        this.code = 200;
        this.message = "Success";
    }

    public ResponseEntity(Integer code, String msg) {
        this.code = code;
        this.message = msg;
        this.result = null;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getResult() {
        return result;
    }

    public void setResult(T result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "ResponseEntity{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", result=" + result +
                '}';
    }

}

拦截器配置

package com.asyf.demo.config;

import com.asyf.demo.intercepter.TokenBucketIntercepter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class MyWebConfig extends WebMvcConfigurationSupport {

    public static final Logger logger = LoggerFactory.getLogger(MyWebConfig.class);

    @Autowired
    private TokenBucketIntercepter tokenBucketIntercepter;

    //注册拦截器
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        System.out.println("===>注册拦截器");
        registry.addInterceptor(tokenBucketIntercepter)
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/swagger-ui.html/**");
        super.addInterceptors(registry);
    }

}

Redis配置类

package com.asyf.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean(name = "redisTemplate")
    public RedisTemplate objRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.findAndRegisterModules();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

}

 maven文件



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.0.RELEASE
         
    
    com.asyf.limitingDemo
    demo
    0.0.1-SNAPSHOT
    demo
    Demo project for Spring Boot

    
        1.8
    

    

        
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
        
            org.apache.commons
            commons-pool2
        

        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            com.alibaba
            fastjson
            1.2.47
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


application.properties配置

#服务器配置
server.port=8080
server.servlet.context-path=/
#工作线程的最大数量
server.tomcat.max-threads=200
#设置服务器连接超时5s
server.connection-timeout=5s
#Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空
spring.redis.password=123456
#Redis数据库索引(默认为0)
spring.redis.database=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=10000
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

三、测试

测试说明:

设置令牌桶容量TOKEN_BUCKET_SIZE=10

启动100个线程同时访问http://127.0.0.1:8080/test,查看结果树。可以看到只有部分接口请求成功,达到了限流的目的。

假如把@LimitingRequired(required = true)注解去掉,重新测试会发现所有请求都会成功。

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第2张图片

 

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第3张图片

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第4张图片

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第5张图片

可以redis客户端查看令牌,程序会以固定的速率添加令牌到令牌桶中。

jmeter测试SpringBoot+Redis令牌桶算法实现接口限流实例_第6张图片

四、总结

限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

本文通过对达到流量上限时拒绝接口的访问,达到了限流的目的。

 

你可能感兴趣的:(SpringBoot)