高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx)
限流算法
常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。
计数器
它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器
滑动窗口计数
滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。
令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
使用RateLimiter实现令牌桶限流
RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。
通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
引入guava的maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
</dependencies>
/**
* 功能说明:使用RateLimiter 实现令牌桶算法
*
*/
@RestController
public class IndexController {
@Autowired
private OrderService orderService;
// 解释:1.0 表示 每秒生成1个令牌存放在桶中
RateLimiter rateLimiter = RateLimiter.create(1.0);
// 下单请求
@RequestMapping("/order")
public String order() {
// 1.限流判断
// 如果在500毫秒内 没有获取到令牌的话,则会一直等待
System.out.println("生成令牌等待时间:" + rateLimiter.acquire());
boolean acquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!acquire) {
System.out.println("你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!");
return "你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!";
}
// 2.如果没有达到限流的要求,直接调用订单接口
boolean isOrderAdd = orderService.addOrder();
if (isOrderAdd) {
return "恭喜您,抢购成功!";
}
return "抢购失败!";
}
}
封装RateLimiter
自定义注解封装RateLimiter
自定义注解
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtRateLimiter {
double value();
long timeOut();
}
编写AOP
@Aspect
@Component
public class RateLimiterAop {
// 存放接口是否已经存在
private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<String, RateLimiter>();
@Pointcut("execution(public * com.itmayeidu.api.*.*(..))")
public void rlAop() {
}
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
// 使用Java反射技术获取方法上是否有@ExtRateLimiter注解类
ExtRateLimiter extRateLimiter = signature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class);
if (extRateLimiter == null) {
// 正常执行方法
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
// ############获取注解上的参数 配置固定速率 ###############
// 获取配置的速率
double value = extRateLimiter.value();
// 获取等待令牌等待时间
long timeOut = extRateLimiter.timeOut();
RateLimiter rateLimiter = getRateLimiter(value, timeOut);
// 判断令牌桶获取token 是否超时
boolean tryAcquire = rateLimiter.tryAcquire(timeOut, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
serviceDowng();
return null;
}
// 获取到令牌,直接执行..
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
// 获取RateLimiter对象
private RateLimiter getRateLimiter(double value, long timeOut) {
// 获取当前URL
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
RateLimiter rateLimiter = null;
if (!rateLimiterMap.containsKey(requestURI)) {
// 开启令牌通限流
rateLimiter = RateLimiter.create(value); // 独立线程
rateLimiterMap.put(requestURI, rateLimiter);
} else {
rateLimiter = rateLimiterMap.get(requestURI);
}
return rateLimiter;
}
// 服务降级
private void serviceDowng() throws IOException {
// 执行服务降级处理
System.out.println("执行降级方法,亲,服务器忙!请稍后重试!");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.println("执行降级方法,亲,服务器忙!请稍后重试!");
} catch (Exception e) {
} finally {
writer.close();
}
}
}
调用
@RequestMapping("/myOrder")
@ExtRateLimiter(value = 10.0, timeOut = 500)
public String myOrder() throws InterruptedException {
System.out.println("myOrder");
return "SUCCESS";
}
漏桶算法
一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;