Guava-RateLimiter实现令牌桶控制接口限流方案
一.前言
对于一个应用系统来说,我们有时会遇到极限并发的情况,即有一个TPS/QPS阀值,如果超了阀值可能会导致服务器崩溃宕机,因此我们最好进行过载保护,防止大量请求涌入击垮系统。对服务接口进行限流可以达到保护系统的效果,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
二.常见限流方案
1.计数器法
原理:在单位时间段内,对请求数进行计数,如果数量超过了单位时间的限制,则执行限流策略,当单位时间结束后,计数器清零,这个过程周而复始,就是计数器法。
缺点:不能均衡限流,在一个单位时间的末尾和下一个单位时间的开始,很可能会有两个访问的峰值,导致系统崩溃。
改进方式:可以通过减小单位时间来提高精度。
2.漏桶算法
原理:假设有一个水桶,水桶有一定的容量,所有请求不论速度都会注入到水桶中,然后水桶以一个恒定的速度向外将请求放出,当水桶满了的时候,新的请求被丢弃。
优点:可以平滑请求,削减峰值。
缺点:瓶颈会在漏出的速度,可能会拖慢整个系统,且不能有效地利用系统的资源。
3.令牌桶算法(推荐)
原理:有一个令牌桶,单位时间内令牌会以恒定的数量(即令牌的加入速度)加入到令牌桶中,所有请求都需要获取令牌才可正常访问。当令牌桶中没有令牌可取的时候,则拒绝请求。
优点:相比漏桶算法,令牌桶算法允许一定的突发流量,但是又不会让突发流量超过我们给定的限制(单位时间窗口内的令牌数)。即限制了我们所说的 QPS(每秒查询率)。
漏桶算法VS令牌桶算法
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
- 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
- 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
- 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
- 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
三.Guava RateLimiter实现平滑限流
Google开源工具包Guava提供了限流工具类RateLimiter,基于令牌桶算法实现。
常用方法:
create(Double permitsPerSecond)方法根据给定的(令牌:单位时间(1s))比例为令牌生成速率
tryAcquire()方法尝试获取一个令牌,立即返回true/false,不阻塞,重载方法具备设置获取令牌个数、获取最大等待时间等参数
acquire()方法与tryAcquire类似,但是会阻塞,尝试获取一个令牌,没有时则阻塞直到获取成功
四.SpringBoot + Interceptor + 自定义注解应用
1.maven依赖
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version> </dependency>
2.自定义注解
1 import java.lang.annotation.*; 2 import java.util.concurrent.TimeUnit; 3 4 /** 5 * RequestLimiter 自定义注解接口限流 6 * 7 * @author xhq 8 * @version 1.0 9 * @date 2019/10/22 16:49 10 */ 11 @Target({ElementType.METHOD}) 12 @Retention(RetentionPolicy.RUNTIME) 13 @Documented 14 public @interface RequestLimiter { 15 16 /** 17 * 每秒创建令牌个数,默认:10 18 */ 19 double QPS() default 10D; 20 21 /** 22 * 获取令牌等待超时时间 默认:500 23 */ 24 long timeout() default 500; 25 26 /** 27 * 超时时间单位 默认:毫秒 28 */ 29 TimeUnit timeunit() default TimeUnit.MILLISECONDS; 30 31 /** 32 * 无法获取令牌返回提示信息 33 */ 34 String msg() default "亲,服务器快被挤爆了,请稍后再试!"; 35 }
3.拦截器
1 import com.google.common.util.concurrent.RateLimiter; 2 import com.mowanka.framework.annotation.RequestLimiter; 3 import com.mowanka.framework.web.result.GenericResult; 4 import com.mowanka.framework.web.result.StateCode; 5 import org.springframework.stereotype.Component; 6 import org.springframework.web.method.HandlerMethod; 7 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse; 10 import java.util.Map; 11 import java.util.concurrent.ConcurrentHashMap; 12 13 /** 14 * 请求限流拦截器 15 * 16 * @author xhq 17 * @version 1.0 18 * @date 2019/10/22 16:46 19 */ 20 @Component 21 public class RequestLimiterInterceptor extends GenericInterceptor { 22 23 /** 24 * 不同的方法存放不同的令牌桶 25 */ 26 private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>(); 27 28 @Override 29 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 30 try { 31 if (handler instanceof HandlerMethod) { 32 HandlerMethod handlerMethod = (HandlerMethod) handler; 33 RequestLimiter rateLimit = handlerMethod.getMethodAnnotation(RequestLimiter.class); 34 //判断是否有注解 35 if (rateLimit != null) { 36 // 获取请求url 37 String url = request.getRequestURI(); 38 RateLimiter rateLimiter; 39 // 判断map集合中是否有创建好的令牌桶 40 if (!rateLimiterMap.containsKey(url)) { 41 // 创建令牌桶,以n r/s往桶中放入令牌 42 rateLimiter = RateLimiter.create(rateLimit.QPS()); 43 rateLimiterMap.put(url, rateLimiter); 44 } 45 rateLimiter = rateLimiterMap.get(url); 46 // 获取令牌 47 boolean acquire = rateLimiter.tryAcquire(rateLimit.timeout(), rateLimit.timeunit()); 48 if (acquire) { 49 //获取令牌成功 50 return super.preHandle(request, response, handler); 51 } else { 52 log.warn("请求被限流,url:{}", request.getServletPath()); 53 this.write(response, new GenericResult(StateCode.ERROR_SERVER, rateLimit.msg())); 54 return false; 55 } 56 } 57 } 58 return true; 59 } catch (Exception var6) { 60 var6.printStackTrace(); 61 this.write(response, new GenericResult(StateCode.ERROR, "对不起,请求似乎出现了一些问题,请您稍后重试!")); 62 return false; 63 } 64 } 65 66 }
4.注册拦截器
1 /** 2 * springboot - WebMvcConfig 3 * 4 * @author xhq 5 * @version 1.0 6 */ 7 @Configuration 8 public class WebMvcConfig implements WebMvcConfigurer { 9 10 /** 11 * 请求限流拦截器 12 */ 13 @Autowired 14 protected RequestLimiterInterceptor requestLimiterInterceptor; 15 16 public WebMvcConfig() {} 17 18 @Override 19 public void addInterceptors(InterceptorRegistry registry) { 20 // 请求限流 21 registry.addInterceptor(requestLimiterInterceptor).addPathPatterns("/**"); 22 } 23 24 }
5.在接口上配置注解
@RequestLimiter(QPS = 5D, timeout = 200, timeunit = TimeUnit.MILLISECONDS,msg = "服务器繁忙,请稍后再试") @GetMapping("/test") @ResponseBody public String test(){ return ""; }
五.总结
1.该代码只适于单个应用进行接口限流,如果是分布式项目或者微服务项目可以采用nosql中央缓存(eg:redis)来实现。
2.除了拦截器,当然也可以用filter和aop来实现。