SpringBoot 接口限流

2024/1/27 SpringBoot 框架

限流是面对大并发大流量的请求下,为了不让系统服务挂掉的一种解决方案。

案例项目地址 (opens new window)

# 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

# 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

# 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

# 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

# 3.5、并发测试

用的测试工具是ApiPost (opens new window),测试1000并发时,请求超过限制之后,系统繁忙,请稍后再试

参考:SpringBoot 如何进行限流? (opens new window)