查看之前的博客可以点击顶部的【分类专栏】
什么是服务限流?
服务限流就是对接口访问进行限制,常用服务限流算法令牌桶、漏桶。计数器也可以进行粗暴限流实现。更多详情可以查看博客:https://blog.csdn.net/BiandanLoveyou/article/details/117376931
本篇博客主要讲解令牌桶算法的限流。
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
使用 RateLimiter 实现令牌桶限流
RateLimiter是 guava (Google)提供的基于令牌桶算法的实现类,可以非常简单的完成接口限流,并且根据系统的实际情况来调整生成token的速率。通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
我们创建一个 limit-server 项目
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study</groupId>
<artifactId>limit-server</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<dependencies>
<!--Spring boot 集成包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
我们创建一个 controller,代码如下:
package com.study.controller;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-21 下午 12:59
*/
@RestController
public class LimitController {
//表示每秒钟生成1个令牌存入令牌桶中
RateLimiter rateLimiter = RateLimiter.create(1.0);
//模拟秒杀下单场景
@GetMapping("/order")
public String order() {
String result = "";
//限制在 500 毫秒内获取令牌,无法获取返回 false
boolean flag = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (flag) {
result = "恭喜您,抢到了一个惊喜!";
//然后实现业务逻辑
} else {
result = "没抢到,等会再试试!";
}
System.out.println(result);
return result;
}
}
bootstrap.yml
server:
port: 80
spring:
application:
name: limit-server
# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
client:
fetch-registry: false
register-with-eureka: false
编写启动类。启动测试:http://127.0.0.1/order
控制台输出:
然后不断的刷新前端页面,测试:
这就是令牌桶算法的结果。
封装 RateLimiter 令牌桶算法
OK,我们尝试封装一下令牌桶算法,只需要在方法里添加一个注解即可。然后令牌桶的存入桶的速率可以配置。我们的令牌桶是针对某一个接口而制定的,也就是说添加了我们自定义的注解之后,就为该接口开辟了令牌桶算法。
我们用到 AOP,因此在 pom.xml 增加依赖:
<!-- springboot-aop 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤1:自定义注解
package com.study.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-21 下午 3:18
*/
@Target(value = ElementType.METHOD)//表示此注解用在方法上
@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
public @interface ExtRateLimiter {
/**
* 每秒添加的数量
* @return
*/
double value();
/**
* 限制等待时间
* @return
*/
long limitTime();
}
步骤2:编写 AOP 切面
package com.study.aop;
import com.google.common.util.concurrent.RateLimiter;
import com.study.annotation.ExtRateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-21 下午 3:24
*/
@Aspect
@Component
public class RateLimiterAOP {
//某个接口对应的令牌桶
private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
//定义切面,拦截 controller 层
@Pointcut("execution(public * com.study.controller.*.*(..))")
public void myPointCut() {
}
//环绕通知(目前主要用于抢单的方法上)
@Around("myPointCut()")
public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
//判断方法上是否有注解 @ExistsApiToken
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
ExtRateLimiter extRateLimiter = methodSignature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class);
//如果不包含该注解,正常往下执行即可
if (extRateLimiter != null) {
//获取注解上的参数,配置固定速率
double value = extRateLimiter.value();
long limitTime = extRateLimiter.limitTime();
RateLimiter rateLimiter = getRateLimiter(value);
//判断令牌桶获取 token 是否超时
boolean acquire = rateLimiter.tryAcquire(limitTime, TimeUnit.MILLISECONDS);
//如果超时
if (!acquire) {
serviceFallBack();
return null;
}
}
return joinPoint.proceed();
}
//获取 RateLimiter 对象
private RateLimiter getRateLimiter(double value) {
//获取当前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 serviceFallBack() throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
String msg = "目前活动太火爆,抢购人数较多,您未能抢到,请稍后重试~";
System.out.println(msg);
try {
writer.println(msg);
} catch (Exception e) {
e.printStackTrace();
} finally {
writer.close();
}
}
}
然后 controller 的代码就简洁了,如下:
package com.study.controller;
import com.study.annotation.ExtRateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-06-21 下午 12:59
*/
@RestController
public class LimitController {
//模拟秒杀下单场景
@GetMapping("/order")
@ExtRateLimiter(value = 2, limitTime = 500)
public String order() {
String result = "恭喜您,抢到了一个惊喜!";
System.out.println(result);
return result;
}
}
重启,测试:
OK,关于高并发服务限流讲解到这。