SpringBoot 接口限流
半塘 2024/1/27 SpringBoot 框架
限流是面对大并发大流量的请求下,为了不让系统服务挂掉的一种解决方案。
# 1、常见的限流算法
# 1.1、计数器限流
计数器限流算法是最为简单粗暴的解决方案,主要用来限制总并发数,比如数据库连接池大小、线程池大小、接口访问并发数等都是使用计数器算法。
如:使用 AomicInteger 来进行统计当前正在并发执行的次数,如果超过域值就直接拒绝请求,提示系统繁忙。
# 1.2、漏桶算法
漏桶算法图解
漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
# 1.3、令牌桶算法
令牌桶算法图解
- 令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
- 系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
# 2、限流方式
# 2.1、单应用实例
应用级限流方式只是单应用内的请求限流,不能进行全局限流。
- 限流总资源数
- 限流总并发/连接/请求数
- 限流某个接口的总并发/请求数
- 限流某个接口的时间窗请求数
- 平滑限流某个接口的请求数
- Guava的RateLimiter限流
# 2.2、分布式
分布式限流和接入层限流可以进行全局限流。
- redis+lua实现中的lua脚本
- 使用Nginx+Lua实现的Lua脚本
- 使用 OpenResty 开源的限流方案
- 限流框架,比如Sentinel实现降级限流熔断
# 3、基于AOP的Guava接口限流
# 3.1、引入依赖
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.2.1</version>
</dependency>
<!-- guava依赖包,限流器 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 3.2、自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Documented
public @interface RateLimit {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "rateLimitKey";
/**
* 最多的访问限制次数
*/
int limitTimes() default 10;
/**
* 获取令牌最大等待时间
*/
long timeout() default 500;
/**
* 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
*/
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
/**
* 得不到令牌的提示语
*/
String errorMdg() default "系统繁忙,请稍后再试";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3.3、AOP处理限流
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
/**
* 不同的接口,不同的流量控制
* map的key为 Limiter.key
*/
private static final ConcurrentHashMap<String, RateLimiter> EXISTED_RATE_LIMITERS = new ConcurrentHashMap<>();
@Around("@annotation(com.xygalaxy.annotation.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿RateLimit的注解
RateLimit limit = method.getAnnotation(RateLimit.class);
if (limit != null) {
// key作用:不同的接口,不同的流量控制
String key = limit.key();
RateLimiter rateLimiter = null;
// 验证缓存是否有命中key
if (!EXISTED_RATE_LIMITERS.containsKey(key)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(limit.limitTimes());
EXISTED_RATE_LIMITERS.put(key, rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.limitTimes());
}
rateLimiter = EXISTED_RATE_LIMITERS.get(key);
// 拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
// 拿不到命令,直接返回异常提示
if (!acquire) {
throw new BusinessException(limit.errorMdg(), BusinessResponseStatus.HTTP_STATUS_10001.getResponseCode());
}
}
return joinPoint.proceed();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 3.4、控制层接口使用
使用@RateLimit
进行限流,当然也可以设置参数,这里就直接用默认参数了。
@RestController
@RequestMapping("/reteLimit")
@Slf4j
@Tag(name = "限流控制层")
public class RateLimitController {
@Operation(description = "测试限流接口")
@RateLimit
@ResponseResultApi
@GetMapping(value = "/test")
public String test(){
return "限流访问成功";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.5、并发测试
用的测试工具是ApiPost (opens new window),测试1000并发时,请求超过限制之后,系统繁忙,请稍后再试