RateLimiter源码解析

目录

简介

平滑预热限流(SmoothWarmingUp)

平滑突发限流(SmoothBursty) 

Demo

create过程

acquire过程

 


简介

限流即流量限制,目的是在遇到流量高峰期或者流量突增时,通过对流量进行限制,以把流量控制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。当达到限制速率时,可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)等。

 Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流。令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,即丢弃或在缓冲区等待。

RateLimiter是一个抽象类,子类SmoothRateLimiter派生了平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)两种实现。继承关系:

 核心参数:

  //当前存储的permits
  double storedPermits;
  //最大可存储permits
  double maxPermits;
  //两个请求的时间间隔,e.g 当permitsPerSecond=5时,那么请求间隔为200ms
  double stableIntervalMicros;
  /*下一次生成令牌的开始时间
    由于RateLimiter允许预消费,上次请求预消费令牌后
    下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌*/
  private long nextFreeTicketMicros = 0L; 

平滑预热限流(SmoothWarmingUp)

启动后会有一段预热期,逐步将分发频率提升到配置的速率。表现形式为令牌刷新的时间间隔由长逐渐变短。等存储令牌数从maxPermits到达thresholdPermits时,发放令牌的时间价格也由coldInterval降低到了正常的stableInterval。

主要原理是分发令牌的速率会随时间和令牌数而改变,由于涉及数学公式推导,先不研究。(想学习的参考这篇 https://cloud.tencent.com/developer/article/1607090

平滑突发限流(SmoothBursty) 

  • Demo

将qps设置为10,每0.1s左右获取一个令牌。

如果设置完qps后没有请求,此时会存储令牌,请求过来后直接拿到令牌不需要等待。比如休眠10s按10qps可以生成100个令牌,但qps10决定了最大存储10个令牌。

  • create过程

  //默认使用平滑突发限流,permitsPerSecond为每秒生成的令牌数
  public static RateLimiter create(double permitsPerSecond) {
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }

  //SleepingStopwatch主要用于计时和休眠
  @VisibleForTesting
  static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond); //RateLimiter的setRate方法
    return rateLimiter;
  }

  public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      //调用SmoothRateLimiter的setRate方法
      doSetRate(permitsPerSecond, stopwatch.readMicros()); //
    }
  }

  //nowMicros为计时器当前时间
  @Override
  final void doSetRate(double permitsPerSecond, long nowMicros) {
    //更新令牌数和获取令牌时间-
    resync(nowMicros);
    //更新请求的时间间隔
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    this.stableIntervalMicros = stableIntervalMicros;
    //第一次调用oldMaxPermits为0,所以storedPermits(桶中令牌个数)也为0
    doSetRate(permitsPerSecond, stableIntervalMicros);
  }

  void resync(long nowMicros) {
    //若当前时间 > 下次生成令牌开始时间
    if (nowMicros > nextFreeTicketMicros) {
      //计算出这段时间内,一共可以生产多少令牌。coolDownIntervalMicros()返回stableIntervalMicros,创建时还未赋值,所以是初始0,newPermits计算结果为无穷大
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      //更新存储令牌数
      storedPermits = min(maxPermits, storedPermits + newPermits);
      //更新下次生成令牌开始时间
      nextFreeTicketMicros = nowMicros;
    }
  }

  //SmoothBursty的coolDownIntervalMicros方法直接返回时间间隔
  @Override
    double coolDownIntervalMicros() {
      return stableIntervalMicros;
    }

   @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
      //更新最大令牌数
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        //重新计算存储的令牌数,保持与最大令牌数的比例
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }

 创建完成后,结果如图所示:当前存储令牌0,最大存储令牌为2,两个请求的时间间隔500ms,更新了下次请求时间,maxBurstSeconds是SmoothBursty的属性,表示存储多少s的请求。

  • acquire过程

RateLimiter会累积令牌,所以可以应对突发流量。在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受。

获取令牌时可以分为三种情况:1、令牌数足够,直接获取令牌并更新存储令牌数、生成令牌开始时间 2、当前时间 > 开始生成令牌时间,但令牌数不够,此时会借用令牌,将存储置为0,并计算借用的令牌所需要偿还的时间,保存到下一次开始生成令牌时间 3、当前时间 < 开始生成令牌时间,即上一个请求借用的令牌还没还完时间,此时需要休眠到偿还完时间为止,并再次借用令牌。

public double acquire() {
    return acquire(1);
}

public double acquire(int permits) {
	//需要等待的时间,当令牌足够或者不够导致第一次借用时,返回为0;
    //当上次借的令牌花费的时间还没有还完,则一直休眠到还完为止,并再次借用
    long microsToWait = reserve(permits);
    //线程休眠
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

final long reserve(int permits) {
    checkPermits(permits);
    //通过互斥锁保证线程安全
    synchronized (mutex()) {
      //计算需要休眠的时间
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
}

final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    //之所以要减去当前时间,是为了让第一次借用令牌的请求不需要休眠,所以后面的方法对nextFreeTicketMicros的处理比较绕
    return max(momentAvailable - nowMicros, 0);
}

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    //当令牌数不够,需要借用时,计算借用的令牌需要花费的时间
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    //更新令牌数
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
}

void resync(long nowMicros) {
    //如果当前时间>令牌开始生成时间,更新令牌数,更新下次令牌开始生成时间
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;
    }
  }

 

与信号量对比:

一旦从 RateLimiter 获得许可,不需要释放。信号量分配后必须释放。
RateLimiter 控制的是速率,以配置的速率分发许可,速率不变时单位时间内分发的许可量是恒定的。信号量控制的是并发访问的数量,单位时间内分配的次数跟使用许可的时长有关,每次申请使用的时间越短,则单位时间内能分配的次数就越多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Spring Boot中关闭RateLimiter,你可以采取以下步骤: 1. 导入所需的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> ``` 2. 创建一个自定义注解,用于标记需要限流的方法: ```java import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimited { String value() default ""; } ``` 3. 创建一个切面类,用于实现限流逻辑: ```java import com.google.common.util.concurrent.RateLimiter; 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.springframework.stereotype.Component; @Aspect @Component public class RateLimitAspect { private final RateLimiter rateLimiter = RateLimiter.create(10); // 设置每秒最大访问次数 @Pointcut("@annotation(com.example.RateLimited)") public void rateLimited() {} @Around("rateLimited()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { if (rateLimiter.tryAcquire()) { return joinPoint.proceed(); } else { throw new RuntimeException("请求过于频繁,请稍后再试!"); } } } ``` 4. 在需要进行限流的方法上添加自定义注解: ```java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { @RateLimited // 添加此注解进行限流 @GetMapping("/my-api") public String myApi() { // 处理业务逻辑 return "Hello World!"; } } ``` 这样,当请求频率超过每秒最大访问次数时,将会抛出一个RuntimeException。你可以根据实际需求进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值