Spring Boot - 实用功能09 - 接口限流

接口限流

一:前置知识

1:为什么要限流

每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。

限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性

系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。

2:限流有哪些常见思路?

从算法上看:令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。

单实例

应用级限流方式只是单应用内的请求限流,不能进行全局限流。

  1. 限流总资源数
  2. 限流总并发/连接/请求数
  3. 限流某个接口的总并发/请求数
  4. 限流某个接口的时间窗请求数
  5. 平滑限流某个接口的请求数
  6. Guava RateLimiter

分布式

我们需要分布式限流和接入层限流来进行全局限流。

  1. redis+lua实现中的lua脚本
  2. 使用Nginx+Lua实现的Lua脚本
  3. 使用 OpenResty 开源的限流方案
  4. 限流框架,比如Hystrix,Sentinel实现降级限流熔断

二:限流算法

1:令牌桶(重点)

一:概述

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法

先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加【令牌桶就是生产令牌的桶】

当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务

在这里插入图片描述
在这里插入图片描述
这里如果一段时间没有请求时,桶内就会积累一些Token,下次一旦有突发流量,只要Token足够,也能一次处理

所以令牌桶算法的特点是允许突发流量

二:举例

Guava RateLimiter - 平滑突发限流(SmoothBursty)

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>
public static void test01() {
    // RateLimiter.create(5)表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌
    RateLimiter rateLimiter = RateLimiter.create(5);

    // imiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0)
    // 如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌
    // 这种实现将突发请求速率平均为了固定请求速率。
    // 如果结构不想等待可以采用tryAcquire立刻返回!
    System.out.println(rateLimiter.acquire());
    System.out.println(rateLimiter.acquire());
    System.out.println(rateLimiter.acquire());
    System.out.println(rateLimiter.acquire());
    System.out.println(rateLimiter.acquire());
    System.out.println(rateLimiter.acquire());
}


// rateLimiter应对突发状况
public static void test02() {
    // limiter.acquire(5)表示桶的容量为5且每秒新增5个令牌
    RateLimiter rateLimiter = RateLimiter.create(5);

    // 令牌桶算法允许一定程度的突发,所以可以一次性消费5个令牌,但接下来的limiter.acquire(1)将等待差不多1秒桶中才能有令牌
    System.out.println(rateLimiter.acquire(5));
    System.out.println(rateLimiter.acquire(1)); // 所以这个打印的是接近1s的数据
    System.out.println(rateLimiter.acquire(1)); // 接下来的请求也整形为固定速率了
}


// 令牌充足模拟
public static void test03() {
    // 创建了一个桶容量为2且每秒新增2个令牌, 500ms一个
    RateLimiter rateLimiter = RateLimiter.create(2);
    System.out.println(rateLimiter.acquire());  // 0
    try {
        Thread.sleep(2000L); // 睡2s
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(rateLimiter.acquire()); // 0
    System.out.println(rateLimiter.acquire()); // 0
    System.out.println(rateLimiter.acquire()); // 0
    System.out.println(rateLimiter.acquire()); // 大概500ms
    System.out.println(rateLimiter.acquire()); // 大概500ms
}

SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间的,另外需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。

另外RateLimiter还提供了tryAcquire方法来进行无阻塞或可超时的令牌消费。

Guava RateLimiter - SmoothWarmingUp

因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。

因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)。

Guava也提供了SmoothWarmingUp来实现这种需求类似漏桶算法

// SmoothWarmingUp创建方式:RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)
// permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。
public static void test04() {
    RateLimiter rateLimiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
    for (int i = 0; i < 5; i++) {
        System.out.println(rateLimiter.acquire());
    }
    try {
        Thread.sleep(1000L);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
    for (int i = 0; i < 5; i++) {
        System.out.println(rateLimiter.acquire());
    }
}

在这里插入图片描述

速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率;

然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率

2:漏桶算法

一:概述

水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率)

当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求

可以看出漏桶算法能强行限制数据的传输速率

在这里插入图片描述

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。

因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。

因此,漏桶算法对于存在突发特性的流量来说缺乏效率

二:令牌桶和漏桶对比
  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
  • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
  • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的

3:计数器方式

计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。

计数器也是最简单粗暴的算法

一:采用AtomicInteger

使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。

弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求

二:采用令牌Semaphore

使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。

如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。

相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的

三:采用线程池ThreadPoolExecutor

固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;

三:实现接口限流思路

主要思路:AOP拦截自定义的RateLimit注解,在AOP中通过Guava RateLimiter;

Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>30.0-jre</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <optional>true</optional>
    </dependency>
</dependencies>

1:注解定义和AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int limit() default 10;
}
@Slf4j
@Aspect
@Component
public class RateLimitAspect {

    private final ConcurrentHashMap<String, RateLimiter> EXISTED_RATE_LIMITERS = new ConcurrentHashMap<>();

    // 定义切入点,就是有RateLimit注解的方法
    @Pointcut("@annotation(tech.pdai.ratelimit.guava.config.ratelimit.RateLimit)")
    public void rateLimit() {
    }

    @Around("rateLimit()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 拿到方法信息
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        // 寻找RateLimit注解
        RateLimit annotation = AnnotationUtils.findAnnotation(method, RateLimit.class);

        // get rate limiter
        RateLimiter rateLimiter = EXISTED_RATE_LIMITERS.computeIfAbsent(
            method.getName(), k -> RateLimiter.create(annotation.limit())
        );

        // process
        if (rateLimiter!=null && rateLimiter.tryAcquire()) {
            return point.proceed(); // 执行原生方法
        } else {
            throw new BusinessException("too many requests, please try again later...");
        }
    }
}

2:异常类和响应类

/**
 * 业务异常处理,扩展常规异常
 */
@Slf4j
public class BusinessException extends RuntimeException {
    public BusinessException() {
        super();
    }

    public BusinessException(final String message) {
        super(message);
    }

    public BusinessException(final String message, final Throwable cause) {
        super(message, cause);
    }

    public BusinessException(final Throwable cause) {
        super(cause);
    }
    
    protected BusinessException(final String message, final Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseBody
    @ExceptionHandler(BusinessException.class)
    public ResponseResult<BusinessException> processBusinessException(BusinessException businessException) {
        log.error(businessException.getLocalizedMessage());
        return ResponseResult.fail(null, businessException.getLocalizedMessage()==null
                ? ResponseStatus.HTTP_STATUS_500.getDescription()
                :businessException.getLocalizedMessage());
    }

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public ResponseResult<Exception> processException(Exception exception) {
        log.error(exception.getLocalizedMessage(), exception);
        return ResponseResult.fail(null, ResponseStatus.HTTP_STATUS_500.getDescription());
    }
}
/**
 * 请求结果封装
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class ResponseResult<T> {

    private long timestamp;

    private String status;

    private String message;

    private T data;

    public static <T> ResponseResult<T> success() {
        return success(null);
    }

    public static <T> ResponseResult<T> success(T data) {
        return ResponseResult.<T>builder().data(data)
                .message(ResponseStatus.SUCCESS.getDescription())
                .status(ResponseStatus.SUCCESS.getResponseCode())
                .timestamp(System.currentTimeMillis())
                .build();
    }

    public static <T extends Serializable> ResponseResult<T> fail(String message) {
        return fail(null, message);
    }
    
    public static <T> ResponseResult<T> fail(T data, String message) {
        return ResponseResult.<T>builder().data(data)
                .message(message)
                .status(ResponseStatus.FAIL.getResponseCode())
                .timestamp(System.currentTimeMillis())
                .build();
    }
}
@Getter
@AllArgsConstructor
public enum ResponseStatus {

    SUCCESS("200", "success"),
    FAIL("500", "failed"),

    HTTP_STATUS_200("200", "ok"),
    HTTP_STATUS_400("400", "request error"),
    HTTP_STATUS_401("401", "no authentication"),
    HTTP_STATUS_403("403", "no authorities"),
    HTTP_STATUS_500("500", "server error");

    public static final List<ResponseStatus> HTTP_STATUS_ALL = Collections.unmodifiableList(
            Arrays.asList(HTTP_STATUS_200, HTTP_STATUS_400, HTTP_STATUS_401, HTTP_STATUS_403, HTTP_STATUS_500
            ));

    private final String responseCode;

    private final String description;
}

3:controller接口

@Slf4j
@RestController
public class RateLimitTestController {
    @RateLimit // 将会使用默认的10
    @GetMapping("/limit")
    public ResponseResult<String> limit() {
        log.info("limit");
        return ResponseResult.success();
    }

    @RateLimit(limit = 5)
    @GetMapping("/limit1")
    public ResponseResult<String> limit1() {
        log.info("limit1");
        return ResponseResult.success();
    }

    @GetMapping("/nolimit")
    public ResponseResult<String> noRateLimiter() {
        log.info("no limit");
        return ResponseResult.success();
    }
}

4:接口测试

@SneakyThrows
public static void test(int clientSize) {
    CountDownLatch downLatch = new CountDownLatch(clientSize);
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(clientSize);
    IntStream.range(0, clientSize).forEach(i ->
                                           fixedThreadPool.submit(() -> {
                                               RestTemplate restTemplate = new RestTemplate();
                                               restTemplate.getForObject("http://localhost:8080/limit1", ResponseResult.class);
                                               downLatch.countDown();
                                           })
                                          );
    downLatch.await();
    fixedThreadPool.shutdown();
}

5:说明

上述实现方式只是单实例下一种思路而已,如果细细的看会发现有很多的bug

  1. 首先, EXISTED_RATE_LIMITERS.computeIfAbsent(method.getName(), k -> RateLimiter.create(annotation.limit())) 这行代码中 method.getName()表明是对方法名进行限流的,其实并不合适,应该需要至少加上类名;
  2. 其次, 如果首次运行时访问的请求是一次性涌入的,即EXISTED_RATE_LIMITERS还是空的时候并发请求@RateLimit接口,那么RateLimiter.create是会重复创建并加入到EXISTED_RATE_LIMITERS的,这是明显的bug;
  3. 再者, 上述实现方式按照方法名去限定请求量,对于很多情况下至少需要支持按照IP和方法名,或者其它自定义的方式进行限流。
  4. 其它一些场景支持的参数抽象和封装等

四:ratelimiter-spring-boot-starter

ratelimiter-spring-boot-starter 是基于 redis 的偏业务应用的分布式限流组件,目前支持时间窗口令牌桶 两种限流算法。

使得项目拥有分布式限流能力变得很简单。

限流的场景有很多,常说的限流一般指网关限流,控制好洪峰流量,以免打垮后方应用。

这里突出偏业务应用的分布式限流 的原因,是因为区别于网关限流,业务侧限流可以轻松根据业务性质做到细粒度的流量控制。

比如如下场景:

  • 案例一:有一个公开的 openApi 接口, openApi 会给接入方派发一个 appId,此时,如果需要根据各个接入方的 appId 限流,网关限流就不好做了,只能在业务侧实现
  • 案例二:公司内部的短信接口,内部对接了多个第三方的短信通道,每个短信通道对流量的控制都不尽相同,假设有的第三方根据手机号和短信模板组合限流,网关限流就更不好做了

限流算法说明:

  • 时间窗口限流:偏向控制请求数量,比如每秒请求数量不超过 100,每分钟请求数量不超过 1000,每小时请求数量不超过 10000,每天请求数量不超过 100000。
  • 令牌桶限流:偏向控制请求频率,比如最大请求并发不超过 100,且 QPS 限制在一定范围内,比如 QPS 限制在 50。

致敬每一个开源大佬!!!

1:引入依赖并添加配置信息

<!-- rate limiter -->
<dependency>
    <groupId>com.github.taptap</groupId>
    <artifactId>ratelimiter-spring-boot-starter</artifactId>
    <version>1.3</version>
</dependency>
spring:
  ratelimiter:
    enabled: true # 开启限流
    redis-address: redis://127.0.0.1:6379
    # redis-password: 123456

启用 ratelimiter 的配置必须加,默认不会加载。

2:基本使用

在需要加限流逻辑的方法上,添加注解 @RateLimit

package com.study.study_demo_of_spring_boot.limit.controller;

import com.taptap.ratelimiter.annotation.RateLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 功能描述:基本限流测试
 * </p>
 *
 * @author cui haida
 * @date 2024/04/27/13:23
 */
@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/get")
    // rateInterval -> 用于时间窗口模式,表示时间窗口
    // 表示如果10s内请求到达5个,将会限流
    @RateLimit(rate = 5, rateInterval = "10s") 
    public String get(String name) {
        return "hello";
    }
}

在最基础限流功能使用上,以上三个步骤就已经完成了。

3:@RateLimit 注解说明

属性单位默认值描述
modeenumTIME_WINDOW限流模式,目前可选时间窗口和令牌桶
rate(这个必填)int时间窗口模式表示每个时间窗口内的请求数量、令牌桶模式表示每秒的令牌生产数量
rateIntervalString1s用于时间窗口模式,表示时间窗口
rateExpressionString通过 EL 表达式从 Spring Config 上下文中获取 rate 的值,rateExpression 的优先级比 rate
fallbackFunctionString自定义触发限流时的降级策略方法,默认触发限流会抛 RateLimitException 异常
customKeyFunctionString自定义获取限流 key 的方法
bucketCapacityint用于令牌桶模式,表示令牌桶的桶的大小,这个参数控制了请求最大并发数
bucketCapacityExpressionString通过 EL 表达式从 Spring Config 上下文中获取 bucketCapacity 的值,bucketCapacityExpression 的优先级比 bucketCapacity
requestedTokensint1用于令牌桶模式,表示每次获取的令牌数,一般不用改动这个参数值,除非你知道你在干嘛

@RateLimit 注解可以添加到任意被 spring 管理的 bean 上,不局限于 controller ,service 、repository 也可以。

4:限流粒度key

限流的粒度是通过限流的 key 来做的,在最基础的设置下,限流的 key 默认是通过方法名称拼出来的

key = RateLimiter_ + 类名 + 方法名

除了默认的 key 策略,ratelimiter-spring-boot-starter 充分考虑了业务限流时的复杂性,提供了多种方式。

结合业务特征,达到更细粒度的限流控制。

5:触发限流之后的行为

默认触发限流后 程序会返回一个 http 状态码为 429 的响应,响应值如下:

在这里插入图片描述

同时,响应的 header 里会携带一个 Retry-After 的时间值,单位 s,用来告诉调用方多久后可以重试。

当然这一切都是可以自定义的,进阶用法可以继续往下看

6:进阶使用

6.1:自定义限流的 key

自定义限流 key 有三种方式,当自定义限流的 key 生效时,限流的 key 就变成了(默认的 key + 自定义的 key)

方式一:@RateLimitKey 的方式

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s")
    public String get(@RateLimitKey String name) { // 
        return "get";
    }
}

@RateLimitKey 注解可以放在方法的入参上,要求入参是基础数据类型,上面的例子,如果 name = kl。那么最终限流的 key 如下:

key=RateLimiter_com.taptap.ratelimiter.web.TestController.get-kl

方式二:指定 keys 的方式

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", keys = {"#name"})
    public String get(String name) {
        return "get";
    }

    @GetMapping("/hello")
    @RateLimit(rate = 5, rateInterval = "10s", keys = {"#user.name", "user.id"})
    public String hello(User user) {
        return "hello";
    }
}

keys 这个参数比 @RateLimitKey 注解更智能,基本可以包含 @RateLimitKey 的能力,只是简单场景下,使用起来没有 @RateLimitKey 那么便捷。

keys 的语法来自 spring 的 Spel ,可以获取对象入参里的属性,支持获取多个,最后会拼接起来。

使用过 spring-cache 的可能会更加熟悉 如果不清楚 Spel 的用法,可以参考 spring-cache 的注解文档

方式三:自定义 key 获取函数

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", customKeyFunction = "keyFunction")
    public String get(String name) {
        return "get";
    }

    // 自定义生成key的方法
    public String keyFunction(String name) {
        return "keyFunction" + name;
    }
}

当 @RateLimitKey 和 keys 参数都没法满足时,比如入参的值是一个加密的值,需要解密后根据相关明文内容限流。

可以通过在同一类里自定义获取 key 的函数,这个函数要求和被限流的方法入参一致,返回值为 String 类型。

返回值不能为空,为空时,会回退到默认的 key 获取策略。

6.2:自定义限流后的行为

配置响应内容

spring.ratelimiter.enabled=true
spring.ratelimiter.response-body=Too Many Requests  
spring.ratelimiter.status-code=509

添加如上配置后,触发限流时,http 的状态码就变成了 509 。响应的内容变成了 Too Many Requests 了

自定义限流触发异常处理器

默认的触发限流后,限流器会抛出一个异常,限流器框架内定义了一个异常处理器来处理。

自定义限流触发处理器,需要先禁用系统默认的限流触发处理器,禁用方式如下:

spring.ratelimiter.exceptionHandler.enable=false

然后在项目里添加自定义处理器,如下:

@ControllerAdvice
public class RateLimitExceptionHandler {

    private final RateLimiterProperties limiterProperties;

    public RateLimitExceptionHandler(RateLimiterProperties limiterProperties) {
        this.limiterProperties = limiterProperties;
    }

    @ExceptionHandler(value = RateLimitException.class)
    @ResponseBody
    public String exceptionHandler(HttpServletResponse response, RateLimitException e) {
        response.setStatus(limiterProperties.getStatusCode());
        response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
        return limiterProperties.getResponseBody();
    }
}

自定义触发限流处理函数,限流降级

@RequestMapping("/test")
public class TestController {
    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", fallbackFunction = "getFallback")
    public String get(String name) {
        return "get";
    }
    public String getFallback(String name) {
        return "Too Many Requests" + name;
    }
}

这种方式实现和使用和 自定义 key 获取函数类似。

但是多一个要求,返回值的类型需要和原限流函数的返回值类型一致,当触发限流时,框架会调用 fallbackFunction 配置的函数执行并返回

达到限流降级的效果

6.3:动态设置限流大小

v1.2 版本开始,在 @RateLimit 注解里新增了属性 rateExpression。

该属性支持 Spel 表达式从 Spring 的配置上下文中获取值。

当配置了 rateExpression 后,rate 属性的配置就不生效了。使用方式如下:

@GetMapping("/get2")
@RateLimit(rate = 2, rateInterval = "10s", rateExpression = "${spring.ratelimiter.max}")
public String get2() {
    return"get";
}

集成 apollo 等配置中心后,可以做到限流大小的动态调整在线热更。

6.4:直接使用限流器服务

v1.3 版本开始,限流器框架内部提供了一个限流器服务,可以直接使用。

当使用 RateLimiterService 后,则不用关心限流注解的逻辑了,所有限流逻辑都可以高度定制,如下

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private RateLimiterService limiterService;

    @GetMapping("/limiterService/time-window")
    public String limiterServiceTimeWindow(String key) {
        Rule rule = new Rule(Mode.TIME_WINDOW); // 限流策略,设置为时间窗口
        rule.setKey(key); //限流的 key
        rule.setRate(5); //限流的速率
        rule.setRateInterval(10); //时间窗口大小,单位为秒
        Result result = limiterService.isAllowed(rule);
        if (result.isAllow()) { //如果允许访问
            return "ok";
        } else {
            //触发限流
            return "no";
        }
    }

    @GetMapping("/limiterService/token-bucket")
    public String limiterServiceTokenBucket(String key) {
        Rule rule = new Rule(Mode.TOKEN_BUCKET); // 限流策略,设置为令牌桶
        rule.setKey(key); //限流的 key
        rule.setRate(5); //每秒产生的令牌数
        rule.setBucketCapacity(10); //令牌桶容量
        rule.setRequestedTokens(1); //请求的令牌数
        Result result = limiterService.isAllowed(rule);
        if (result.isAllow()) { //如果允许访问
            return "ok";
        } else {
            //触发限流
            return "no";
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值