RateLimiter实现原理
简介
- 维护 storedPermits、nextFreeTicketMicros 变量,其中 storedPermits代表当前的令牌数目,而 nextFreeTicketMicros 代表获取到下一个令牌的时间。
- 是一种借用、惰性更新、计算睡眠时间的一种算法。其中的借用,是指对于第一个请求会 先借用令牌桶中没生成好的 若干个令牌直接返回 ,而第二个请求 不管当前令牌桶有多少令牌,也要以固定速率补充回被借用的令牌 后才可以执行,而第三个,也是需要以固定速率补充回第二个借用的令牌才可以执行。而惰性更新,是指不通过定时任务一直更新令牌桶的数目,而是每次请求来执行resync()来计算有多少个令牌准备好了。而计算睡眠时间,则是不通过自旋,或是任务队列挂起唤醒这些做法,而是计算每个请求需要的睡眠时间,做线程睡眠自动苏醒,相当于排队。
- 优点: 不通过竞争令牌桶中的令牌实现,而是计算等待时间的方式,实现比较简单。同时避免频繁的线程切换和锁竞争,提高性能。
- 缺点: 借用的思路有可能导致消耗令牌的速率不均匀。依赖系统时间,系统时间的调整可能会影响到正确性。
代码整理
- 在 acquire / tryAcquire() / create()的doSetRate() 中,对 mutex() 加锁, 这个就是细粒度的一个锁。只阻塞this的这个acquire / tryAcquire()方法。
- 在执行acquire / tryAcquire()中,同步块中
- resync()
- 惰性更新 落后的nextFreeTicketMicros 为当前时间, 也就是如果当前时间已经大于nextFreeTicketMicros,则更新 nextFreeTicketMicros 为当前时间
- 惰性更新 storedPermits 加上(当前时间-落后的nextFreeTicketMicros)过程中可以生成多少令牌,作为存储的令牌,不会超过 maxPermits 最大令牌数
- reserveAndGetWaitLength()
- 计算 nextFreeTicketMicros - 当前时间,代表这个请求需要等待的时间: microsToNextFreeTicket
- 根据当前令牌桶里的数目,计算还需要多少时间才能偿还 被借用的令牌数目 ,并叠加到下次请求的放行时间:nextFreeTicketMicros
- 返回 microsToNextFreeTicket
- 睡眠 waitMicros 这段时间 Thread.sleep()
- resync()
- 在执行create()中,
- doSetRate() 已经在同步代码中
- 执行resync(),惰性更新 nextFreeTicketMicros 和 storedPermits。
- maxPermits = maxBurstSeconds * permitsPerSecond; 即 最大许可为1秒钟生成多少个许可。作为最大令牌。
- 当每秒生成令牌小于1时,相当于系统不会存储令牌。
- doSetRate() 已经在同步代码中
结合源码
demo
public void contextLoad5() throws InterruptedException { // 每秒0.1个请求, 阻塞直到获取到令牌 RateLimiter rateLimiter = RateLimiter.create(0.1); CountDownLatch countDownLatch = new CountDownLatch(10); ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(() -> { int times = 0; while (true) { boolean b = rateLimiter.tryAcquire(); if (b) { times++; System.out.println(Thread.currentThread().getName() + "当前请求i:【" + times + "】"); } else { System.out.println(Thread.currentThread().getName() + "表示已达到限流阈值,拒绝请求..." + times); } try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } countDownLatch.await(); }
RateLimiter.create(0.1);
// 当前令牌数目 double storedPermits; // 最大令牌数目 double maxPermits; // 每个请求需要多少 volatile double stableIntervalMicros; // 锁 : acquire / tryAcquire / setRate 的锁 private final Object mutex; // 下一个请求可以放行的时间 private long nextFreeTicketMicros; // permitsPerSecond: 每秒钟生成多少个请求 static RateLimiter create(RateLimiter.SleepingTicker ticker, double permitsPerSecond) { RateLimiter rateLimiter = new RateLimiter.Bursty(ticker, 1.0D); rateLimiter.setRate(permitsPerSecond); return rateLimiter; } public final void setRate(double permitsPerSecond) { Preconditions.checkArgument(permitsPerSecond > 0.0D && !Double.isNaN(permitsPerSecond), "rate must be positive"); // 使用 mutex 作为锁 synchronized(this.mutex) { // 落后的nextFreeTicketMicros ,意味着 nowMicros > this.nextFreeTicketMicros // 惰性更新 落后的nextFreeTicketMicros 为当前时间, 也就是如果当前时间已经大于nextFreeTicketMicros,则更新 nextFreeTicketMicros 为当前时间 // 惰性更新 storedPermits 加上(当前时间-落后的nextFreeTicketMicros)过程中可以生成多少令牌,作为存储的令牌,不会超过 maxPermits 最大令牌数 this.resync(this.readSafeMicros()); // 生成一个令牌需要的时间(微秒): stableIntervalMicros = 1 * 1000 * 1000 / 每秒钟生成多少个请求 double stableIntervalMicros = (double)TimeUnit.SECONDS.toMicros(1L) / permitsPerSecond; this.stableIntervalMicros = stableIntervalMicros; // 生成最大令牌数目: maxPermits = 1L * 每秒钟生成多少个请求 this.doSetRate(permitsPerSecond, stableIntervalMicros); } } private void resync(long nowMicros) { if (nowMicros > this.nextFreeTicketMicros) { this.storedPermits = Math.min(this.maxPermits, this.storedPermits + (double)(nowMicros - this.nextFreeTicketMicros) / this.stableIntervalMicros); this.nextFreeTicketMicros = nowMicros; } } void doSetRate(double permitsPerSecond, double stableIntervalMicros) { double oldMaxPermits = this.maxPermits; this.maxPermits = this.maxBurstSeconds * permitsPerSecond; this.storedPermits = oldMaxPermits == 0.0D ? 0.0D : this.storedPermits * this.maxPermits / oldMaxPermits; }
boolean b = rateLimiter.tryAcquire();
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { long timeoutMicros = unit.toMicros(timeout); // 校验一下permits是不是负数 checkPermits(permits); long microsToWait; // 使用 mutex 作为锁 synchronized(this.mutex) { long nowMicros = this.readSafeMicros(); // 如果 下一个请求可以放行的时间 大于 当前时间+允许的偏移时间, 说明还没到时间,不能放行 if (this.nextFreeTicketMicros > nowMicros + timeoutMicros) { return false; } // 否则就是已经可以放行了 // 计算 nextFreeTicketMicros - 当前时间,代表这个请求需要等待的时间,作为结果返回microsToWait // 同时叠加下一个请求需要的时间 加上 偿还本次借用的令牌需要的时间 microsToWait = this.reserveNextTicket((double)permits, nowMicros); } // 进行等待 microsToWait, Thread.sleep 一段时间 this.ticker.sleepMicrosUninterruptibly(microsToWait); // 获取令牌成功 return true; } // requiredPermits : 这次请求总共需要多少个令牌 private long reserveNextTicket(double requiredPermits, long nowMicros) { // 落后的nextFreeTicketMicros ,意味着 nowMicros > this.nextFreeTicketMicros // 惰性更新 落后的nextFreeTicketMicros 为当前时间 // 惰性更新 storedPermits 为 (当前时间-落后的nextFreeTicketMicros)过程中产生的 令牌数目 this.resync(nowMicros); // microsToNextFreeTicket (需要等待的时间) = 下一个请求可以放行的时间 - 当前时间 long microsToNextFreeTicket = this.nextFreeTicketMicros - nowMicros; // storedPermitsToSpend (对于这次请求,当下可以立刻提供多少个令牌) = Math.min(requiredPermits, this.storedPermits); double storedPermitsToSpend = Math.min(requiredPermits, this.storedPermits); // freshPermits (对于这次请求,还需要提供多少个令牌) = requiredPermits - storedPermitsToSpend; double freshPermits = requiredPermits - storedPermitsToSpend; // 需要等待的时间 = 从 storedPermits 中借出 storedPermitsToSpend 需要的时间(默认为0) + 还需要多少个令牌 * 生成一个令牌需要的时间 long waitMicros = this.storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long)(freshPermits * this.stableIntervalMicros); // 更新 下一个请求可以放行的时间 加上 重新生成借用的令牌需要的时间 this.nextFreeTicketMicros += waitMicros; // 令牌消耗 this.storedPermits -= storedPermitsToSpend; return microsToNextFreeTicket; } long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { return 0L; }
rateLimiter.acquire();
public double acquire(int permits) { // 校验一下permits是不是负数 checkPermits(permits); long microsToWait; // 使用 mutex 作为锁 synchronized (mutex) { // 获取这个请求需要等待多长时间,更新下次请求需要等待的时间 microsToWait = reserveNextTicket(permits, readSafeMicros()); } // 进行等待 microsToWait, Thread.sleep 一段时间 ticker.sleepMicrosUninterruptibly(microsToWait); return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L); }