限流算法及 RateLimiter 的使用和代码解读

为什么要限流

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。

互联网雪崩效应解决方案

服务降级:在高并发的情况, 防止用户一直等待,直接返回一个友好的错误提示给客户端。
服务熔断:在高并发的情况,一旦达到服务最大的承受极限,直接拒绝访问,使用服务降级。
服务隔离:

  • 雪崩效应产生原因:因为默认情况下,只有一个线程池维护所有的服务接口,如果有大量的请求访问同一个接口,达到线程池默认极限,可能会导致其他服务无法访问。
  • 解决服务雪崩效应:使用服务隔离机制,使用线程池方式隔离,相当于每个接口(服务)都有自己独立的线程池,因为每个线程池互不影响,这样就可以解决雪崩效应。

服务限流:在高并发的情况,一旦服务承受不了,使用服务限流机制

限流算法

常见的限流算法有:令牌桶、漏桶。计数器也可以简单粗暴地实现限流。

计数器

它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,3秒钟内的请求不能超过3次。
我们可以在开始时设置一个计数器,每次请求,该计数器+1;
如果该计数器的值大于3并且与第一次请求的时间间隔在3秒钟内,那么说明请求过多,应该限流;
如果该请求与第一次请求的时间间隔大于3秒钟,并且该计数器的值还在限流范围内,那么重置该计数器。

/**
 * 计数器限流
 */
public class LimitCountDemo {
    private int limtCount = 3;// 限制最大访问的容量
    AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒钟 实际请求的数量
    private boolean isReset = false;
    private long start = System.currentTimeMillis();// 获取当前系统时间
    private int interval = 3000;// 间隔时间
    private final ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); // 线程池

    private static final Logger logger = LoggerFactory.getLogger(LimitCountDemo.class);
    private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");

    private synchronized boolean acquire() {
        long newTime = System.currentTimeMillis();
        if (newTime > (start + interval) && !isReset) {
            isReset = true;
            // 判断是否是一个周期
            start = newTime;
            logger.info("start=" + sdf.format(new Date(start)));
            atomicInteger.set(0); // 清理为0
        }
        if (isReset) {
            isReset = false;
        }
        int i = atomicInteger.incrementAndGet();// i++;
        logger.info("atomicInteger=" + i);
        return i <= limtCount;
    }

    private void access() {
        for (int i = 1; i <= 6; i++) {
            final int tempI = i;
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    if (acquire()) {
                        logger.info("你没有被限流,可以正常访问逻辑 i:" + tempI);
                    } else {
                        logger.info("你已经被限流呢  i:" + tempI);
                    }
                }
            });
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LimitCountDemo limitService = new LimitCountDemo();
        logger.info("start=" + sdf.format(new Date(limitService.start)));
        limitService.access();
        Thread.sleep(3100);
        System.out.println("==================");
        limitService.access();
        Thread.sleep(3100);
        System.out.println("==================");
        limitService.access();
        limitService.newCachedThreadPool.shutdown();
    }
}

令牌桶算法

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。描述如下:

  1. 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
  2. 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
  3. 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
  4. 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

限流算法及 RateLimiter 的使用和代码解读_第1张图片

使用 RateLimiter 实现令牌桶限流

RateLimiter 是 guava 提供的基于令牌桶算法的实现类,可以非常简单地实现服务限流,并且根据系统的实际情况来调整生成 token 的速率。

通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。

下面来看个简单的 RateLimiter 使用 Demo。


<dependency>
	<groupId>com.google.guavagroupId>
	<artifactId>guavaartifactId>
	<version>25.1-jreversion>
dependency>
/**
 * 使用RateLimiter 实现令牌桶算法
 */
@RestController
public class RateLimiterDemo {
    private static final Logger logger = LoggerFactory.getLogger(RateLimiterDemo.class);

    // 1.0 表示 每秒中生成1个令牌存放在桶中
    private RateLimiter rateLimiter = RateLimiter.create(1); // 独立线程

    // 下单请求
    @RequestMapping("/addOrder")
    public String addOrder() {
        // 1.限流判断
        // 如果在500毫秒内如果没有获取到令牌的话,则直接走服务降级处理
        boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
        if (!tryAcquire) {
            logger.info("别抢了, 在抢也是一直等待的, 还是放弃吧!!!");
            return "别抢了, 在抢也是一直等待的, 还是放弃吧!!!";
        } else {
            // 2. 业务逻辑处理
            // 3. 返回结果
            logger.info("恭喜您,抢购成功! ");
            return "恭喜您,抢购成功!";
        }
    }
}

漏桶算法

漏桶算法的描述如下:

  1. 一个固定容量的漏桶,按照常量固定速率流出水滴;
  2. 如果桶是空的,则不需流出水滴;
  3. 可以以任意速率流入水滴到漏桶;
  4. 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

限流算法及 RateLimiter 的使用和代码解读_第2张图片

漏桶算法与令牌桶算法区别?

主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。

RateLimiter 源码解读

Guava 的 RateLimiter 有两种模式:

  1. 稳定模式,SmoothBursty,令牌生成的速度恒定。
  2. 预热模式,SmoothWarmingUp,令牌生成速度缓慢提升直到维持在一个稳定值。比如在系统刚启动的时候,第一次访问的接口需要加载缓存等等,这时系统的处理速度较慢,就需要这种模式。

这里主要讲 SmoothBursty 的源码。需要注意的是,RateLimiter 的实现是可以预支消费令牌,比如当桶里有一个令牌的时候,第一次请求可以获取1个令牌或者100个令牌都不需要等待,但是下一次请求就需要等待到至少剩余一个令牌才可以消费。

首先来看下它的几个属性

/** The currently stored permits. */
/** 当前存储的令牌数 */
  double storedPermits;

/** The maximum number of stored permits. */
/** 最大可存储令牌数 */
  double maxPermits;

/**
   * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
   * per second has a stable interval of 200ms.
   * 产生一个令牌的时间间隔
   */
  double stableIntervalMicros;

  /**
   * The time when the next request (no matter its size) will be granted. After granting a request,
   * this is pushed further in the future. Large requests push this further than small requests.
   * 下次可以拿到令牌的时间
   */
  private long nextFreeTicketMicros = 0L; // could be either in the past or future

/** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
/** 限流器空闲时,保存多少秒产生的令牌器 */
    final double maxBurstSeconds;

调用 create 方法时,会执行下面几个步骤:

  1. 创建一个秒表实例
  2. 实例化 SmoothBursty 限流类,maxBurstSeconds = 1.0
  3. 更新存储的令牌数和下次可以拿到令牌的时间
  4. 设置产生一个令牌的时间间隔
  5. 设置最大可存储令牌数
  6. 设置当前存储的令牌数
  public static RateLimiter create(double permitsPerSecond) {
	return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }
  
  @VisibleForTesting
  static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

  @Override
  final void doSetRate(double permitsPerSecond, long nowMicros) {
    resync(nowMicros);
    double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
    this.stableIntervalMicros = stableIntervalMicros;
    doSetRate(permitsPerSecond, stableIntervalMicros);
  }

  public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      doSetRate(permitsPerSecond, stopwatch.readMicros());
    }
  }
  
    @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;
      }
    }

根据令牌桶算法,桶中的令牌是持续生成存放的,如果开启一个定时任务来生成令牌,这将会消耗大量的资源。而这里是通过延迟计算,resync 方法调用时,当前时间如果晚于下次可以拿到令牌的时间,则更新存储的令牌数和下次可以拿到令牌的时间。

/** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
  void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;
    }
  }

tryAcquire 方法的功能是在指定等待的时间内获取指定数量的令牌,如果不能获取到,则直接返回 false,如果能获取,则阻塞等待,然后再返回 true

  public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros();
      if (!canAcquire(nowMicros, timeoutMicros)) {
        return false;
      } else {
        microsToWait = reserveAndGetWaitLength(permits, nowMicros);
      }
    }
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
  }

canAcquire 返回指定的时间内,能否获取令牌

/**
* 下次可以拿到令牌的时间 <= 当前时间 + 等待的时间
*/
  private boolean canAcquire(long nowMicros, long timeoutMicros) {
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
  }
  
  @Override
  final long queryEarliestAvailable(long nowMicros) {
    return nextFreeTicketMicros;
  }

获取需要等待的时间

 /**
   * Reserves next ticket and returns the wait time that the caller must wait for.
   * @return the required wait time, never negative
   */
  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    // 需要等待的时间 = 下次可以拿到令牌的时间 - 当前时间
    return max(momentAvailable - nowMicros, 0);
  }

返回下次可以拿到令牌的时间点:

  1. 根据当前时间更新存储的令牌数和下次可以拿到令牌的时间
  2. 返回下次可以拿到令牌的时间
  3. 更新下次可以拿到令牌的时间,加上等待预支令牌的产生时间
  4. 更新存储的令牌数,减去被消费的令牌数
/**
   * Reserves the requested number of permits and returns the time that those permits can be used
   * (with one caveat).
   *
   * @return the time that the permits may be used, or, if the permits may be used immediately, an
   *     arbitrary past or present time
   */
  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;
  }

限流算法及 RateLimiter 的使用和代码解读_第3张图片

参考

  • Demo 代码
  • 参考每特教育-余老师文档
  • Guava API Docs
  • Guava RateLimiter源码解析

你可能感兴趣的:(限流算法及 RateLimiter 的使用和代码解读)