1. 限流
每个服务器都有访问上限,当访问的并发量大过服务器的承受范围的时候,我们就需要考虑限流的方式 确保系统挂掉。
2.限流算法
常见的限流算法有两种:漏桶算法和令牌桶算法。
2.1 漏桶算法
算法思路: 将请求先加入到桶里,漏桶按照一定的速度将发出 处理请求,当请求并发量过大时候,超出桶的容量就会拒绝请求。
2.2 令牌桶
算法思路: 系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token ,如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.
RateLimiter主要接口
ateLimiter其实是一个abstract类,但是它提供了几个static方法用于创建RateLimiter:
创建限流器
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
*/
public static RateLimiter create(double permitsPerSecond);
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
*
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);
获取令牌
阻塞
~~java
不带参数表示获取一个令牌.如果没有令牌则一直等待
public double acquire();
public double acquire(int permits);
//不阻塞 尝试获取令牌
~~~java
public boolean tryAcquire();
//尝试获取一个令牌,立即返回
public boolean tryAcquire(int permits);
public boolean tryAcquire(long timeout, TimeUnit unit);
//尝试获取permits个令牌,带超时时间
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);
springboot 拦截器+RateLimiter 实现限流 代码
package com.example.demo.Advice.LimitInterceptor;
import com.example.demo.exception.APIException;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;
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.util.concurrent.TimeUnit;
/**
* Guava 限流器
* @author linjunbo
*
*/
@Component
public class LimitInterceptor extends HandlerInterceptorAdapter {
public enum LimitType {
DROP, //丢弃
WAIT //等待
}
/**
* Guava 开源工具限流工具类
* 限流器
*/
private RateLimiter limiter;
/**
* 限流方式
*/
private LimitType limitType = LimitType.DROP;
public LimitInterceptor() {
this.limiter = RateLimiter.create(1);
}
/**
* @param tps 限流(每秒处理量)
* @param limitType
*/
public LimitInterceptor(int tps, LimitInterceptor.LimitType limitType) {
this.limiter = RateLimiter.create(tps);
this.limitType = limitType;
}
/**
* @param permitsPerSecond 每秒新增的令牌数
* @param limitType 限流类型
*/
public LimitInterceptor(double permitsPerSecond, LimitInterceptor.LimitType limitType) {
this.limiter = RateLimiter.create(permitsPerSecond, 1000, TimeUnit.MILLISECONDS);
this.limitType = limitType;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (limitType.equals(LimitType.DROP)) {
//尝试获取一个令牌,立即返回
if (limiter.tryAcquire()) {
return super.preHandle(request, response, handler);
}
}
// else {
// //获取令牌 如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0:
// double count = limiter.acquire();
// return super.preHandle(request, response, handler);
// }
throw new APIException(500,"服务器达到请求达到上限,限流生效");//达到限流后,往页面提示的错误信息。
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
}
public RateLimiter getLimiter() {
return limiter;
}
public void setLimiter(RateLimiter limiter) {
this.limiter = limiter;
}
}
@Component
public class LimitFilter implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//每秒只能接收100个请求
registry.addInterceptor(new LimitInterceptor(100,LimitInterceptor.LimitType.DROP))
.addPathPatterns("/**")
//忽略拦截
.excludePathPatterns("/login");
}
}
使用JMETER 测试
结果如图