在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
1缓存 缓存的目的是提升系统访问速度和增大系统处理容量
2降级 降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
3限流 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处
涉及到的技术:
(1)SpringBoot拦截器
(2)ScheduledThreadPoolExecutor定时任务
(3)Redis队列
(4)SpringBoot RedisTemplate操作redis
(5)jmeter测试工具
使用令牌桶算法实现接口限流
令牌桶算法(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
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)注解去掉,重新测试会发现所有请求都会成功。
可以redis客户端查看令牌,程序会以固定的速率添加令牌到令牌桶中。
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
本文通过对达到流量上限时拒绝接口的访问,达到了限流的目的。