常见限流方案
1.计数器法
- 原理:在单位时间段内,对请求数进行计数,如果数量超过了单位时间的限制,则执行限流策略,当单位时间结束后,计数器清零,这个过程周而复始,就是计数器法。
- 缺点:不能均衡限流,在一个单位时间的末尾和下一个单位时间的开始,很可能会有两个访问的峰值,导致系统崩溃。
- 改进方式:可以通过减小单位时间来提高精度。
2.漏桶算法
- 原理:假设有一个水桶,水桶有一定的容量,所有请求不论速度都会注入到水桶中,然后水桶以一个恒定的速度向外将请求放出,当水桶满了的时候,新的请求被丢弃。
- 优点:可以平滑请求,削减峰值。
- 缺点:瓶颈会在漏出的速度,可能会拖慢整个系统,且不能有效地利用系统的资源。
3.令牌桶算法(推荐)
- 原理:有一个令牌桶,单位时间内令牌会以恒定的数量(即令牌的加入速度)加入到令牌桶中,所有请求都需要获取令牌才可正常访问。当令牌桶中没有令牌可取的时候,则拒绝请求。
- 优点:相比漏桶算法,令牌桶算法允许一定的突发流量,但是又不会让突发流量超过我们给定的限制(单位时间窗口内的令牌数)。即限制了我们所说的 QPS(每秒查询率)。
实现
1.依赖
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>27.1-jre</version>
</dependency>
2.注解类
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** @author EDZ* @Classname RequestLimiter* @Description TODO* @Date 2020/6/29 17:46* @Created zzf*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimiter {/*** 每秒创建令牌个数,默认:10*/double QPS() default 10D;/*** 获取令牌等待超时时间 默认:500*/long timeout() default 500;/*** 超时时间单位 默认:毫秒*/TimeUnit timeunit() default TimeUnit.MILLISECONDS;/*** 无法获取令牌返回提示信息*/String msg() default "服务器繁忙,请稍后重试!";
}
3.定义一个枚举类用于状态返回
/*** @Classname ResponseEnum* @Description TODO* @Date 2020/6/29 17:52* @Created zzf*/
public enum ResponseEnum {SUCCESS("200", "000000", "请求成功"),FAIL("200", "100000", "请求失败"),FAIL_BY_PARAMS("200", "200000", "请求参数异常"),FAIL_IN_SERVER("200", "300000", "服务器内部异常"),RATE_LIMIT("200", "400000", "限流中");public String status;public String code;public String message;ResponseEnum(String s, String s1, String s2) {this.status = s;this.code = s1;this.message = s2;}
}
4.自定义拦截器,并在拦截器中实现限流
import com.alibaba.fastjson.JSON;
import com.ils.intelab.common.result.CommonResult;
import com.ils.intelab.openapi.client.enums.ResponseEnum;
import com.ils.intelab.openapi.client.result.OpenApiResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;/*** 抽象拦截器*/
public abstract class AbstractInterceptor extends HandlerInterceptorAdapter {private Logger logger = LoggerFactory.getLogger(AbstractInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {ResponseEnum result;try {result = preFilter(request, response, handler);} catch (Exception e) {logger.error("preHandle catch a exception:" + e.getMessage());result = ResponseEnum.FAIL;}if (ResponseEnum.SUCCESS.code.equals(result.code)) {return true;}handlerResponse(result, response);return false;}/*** 自定义pre处理** @param request* @return*/protected abstract ResponseEnum preFilter(HttpServletRequest request, HttpServletResponse response, Object handler);/*** 错误处理事件** @param result* @param response*/private void handlerResponse(ResponseEnum result, HttpServletResponse response) {CommonResult commonResult = new CommonResult();commonResult.setData(null);CommonResult<Object> errorInfo = new CommonResult<>();errorInfo.setCodes(OpenApiResultCode.ERROR_DEFAULT_NOT_CATCH.getCode());errorInfo.setSuccess(false);errorInfo.setMessage(OpenApiResultCode.errorMessageByLanguage.get("zh").get(OpenApiResultCode.ERROR_DEFAULT_NOT_CATCH.getMessage()));errorInfo.setData(null);response.setStatus(HttpServletResponse.SC_OK);response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);PrintWriter printWriter = null;try {printWriter = response.getWriter();printWriter.write(JSON.toJSONString(errorInfo));} catch (Exception e) {logger.error("handlerResponse catch a exception:" + e.getMessage());} finally {if (printWriter != null) {printWriter.close();}}}
}
实现
import com.google.common.util.concurrent.RateLimiter;
import com.ils.intelab.openapi.annotation.RequestLimiter;
import com.ils.intelab.openapi.client.enums.ResponseEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 请求限流器* @Classname RequestLimiterInterceptor* @Description TODO* @Date 2020/6/29 17:47* @Created zzf*/
@Slf4j
@Component
public class RequestLimiterInterceptor extends AbstractInterceptor {/*** 不同的方法存放不同的令牌桶*/private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();@Overrideprotected ResponseEnum preFilter(HttpServletRequest request, HttpServletResponse response, Object handler) {try {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;RequestLimiter rateLimit = handlerMethod.getMethodAnnotation(RequestLimiter.class);//判断是否有注解if (rateLimit != null) {// 获取请求urlString key = request.getMethod() + request.getRequestURI();RateLimiter rateLimiter;// 判断map集合中是否有创建好的令牌桶if (!rateLimiterMap.containsKey(key)) {// 创建令牌桶,以n r/s往桶中放入令牌rateLimiter = RateLimiter.create(rateLimit.QPS());rateLimiterMap.put(key, rateLimiter);}rateLimiter = rateLimiterMap.get(key);// 获取令牌boolean acquire = rateLimiter.tryAcquire(rateLimit.timeout(), rateLimit.timeunit());if (acquire) {//获取令牌成功return ResponseEnum.SUCCESS;} else {log.warn("请求被限流,url:{}", request.getServletPath());return ResponseEnum.RATE_LIMIT;}}}return ResponseEnum.SUCCESS;} catch (Exception var6) {var6.printStackTrace();return ResponseEnum.RATE_LIMIT;}}}
5.继承WebMvcConfigurerAdapter来添加自定义拦截器
import com.ils.intelab.openapi.aspect.RequestLimiterInterceptor;
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.WebMvcConfigurer;/*** 注册拦截器*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {/*** 请求限流拦截器*/@Autowiredprotected RequestLimiterInterceptor requestLimiterInterceptor;public WebMvcConfig() {}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 请求限流registry.addInterceptor(requestLimiterInterceptor).addPathPatterns("/**");}}
6.测试
import com.ils.intelab.common.result.CommonResult;
import com.ils.intelab.openapi.annotation.RequestLimiter;
import io.swagger.annotations.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;/*** @Classname TestController* @Description TODO* @Date 2020/5/27 9:50* @Created zzf*/
@RestController
@RequestMapping("/api/v3/")
@Api(value = "open_api", description = "open_api基础接口", tags = {"open_api"})
public class TestController {@ApiOperation(value = "测试", notes = "测试")@ApiResponses({@ApiResponse(code = 400, message = "参数非法"),@ApiResponse(code = 500, message = "服务器错误"),@ApiResponse(code = 200, message = "成功")})@ApiImplicitParams({})@RequestLimiter(QPS = 1, timeout = 200, timeunit = TimeUnit.MILLISECONDS, msg = "服务器繁忙,请稍后再试")@RequestMapping(value = "/test", method = RequestMethod.GET)public CommonResult<String> getUserInfo() {return new CommonResult<>("999");}
}