RateLimiter-源码分析

一、前言

在分布式系统中,实现高可用有三大利器:

  • 限流
  • 降级
  • 熔断
    我们先对限流来进行一个分析。

二、限流的实现

业界常用的限流的实现方式也有多种,我尝试做一个简单的总结:

  • 计数器
    统计单位时间内的请求数量,超过某个阈值进行限流
  • 信号量
    控制请求的最大并发数。Hystrix的信号量模式就是通过控制最大并发数来实现限流。
  • 滑动窗口
    我个人理解是计数器的升级版,通过统计多个连续的桶来得到一个比较精准和合理的值来进行限流。
  • 漏桶算法
    流量会先经过漏桶,从漏桶出来的流量会保证匀速地产生,如果漏桶处理不过来(溢出)的流量认为就是需要限流的流量。
  • 令牌桶算法
    跟令牌桶有点相反,有一个定时器匀速地产生token到桶里面,流量经过的时候会到桶去获取token,获取token失败的流量就被阻塞。如果token一直没有被消费,会累积到桶里面,当然桶本身也有大小,超过最大数量的token会被丢弃掉。

漏桶算法 和 令牌桶算法 最大的区别:

  • 漏桶算法的处理的流量必然是匀速的,但是令牌桶算法是允许有一定的突刺流量(取决于桶的最大大小)

三、RateLimiter的创建

guawa的RateLimiter就是使用令牌桶算法实现的。

RateLimiter

我们先分析构造函数,RateLimiter的create方法生成了一个SmoothBursty的类

public static RateLimiter create(double permitsPerSecond) {
    /*
     * The default RateLimiter configuration can save the unused permits of up to one second. This
     * is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, and 4 threads,
     * all calling acquire() at these moments:
     *
     * T0 at 0 seconds
     * T1 at 1.05 seconds
     * T2 at 2 seconds
     * T3 at 3 seconds
     *
     * Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also
     * have to sleep till 3.05 seconds.
     */
    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;
  }

SmoothBursty其实就是RateLimiter的子类


SmoothBursty类图

继续往下看SmoothBursty,有一个maxBurstSeconds的属性,这个就是桶的最大大小,如果ratelimiter没有试用,桶最大的保存的秒数,默认值为1。

static final class SmoothBursty extends SmoothRateLimiter {
    /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
    final double maxBurstSeconds;

    SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
      super(stopwatch);
      this.maxBurstSeconds = maxBurstSeconds;
    }

继续往下看maxPermits 是如何定义的,桶保存的最大秒数*每秒允许的请求数

@Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
//重点关注这一行,最大允许的请求为maxBurstSeconds * permitsPerSecond。
      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;
      }
    }
/** The maximum number of stored permits. */
  double maxPermits;

四、tryAcquire

我们重点关注tryAcquire,tryAcquire跟acquire的主要区别是tryAcquire不会阻塞,马上返回限流结果,而acquire则会阻塞到获取ticket成功为止(当然可以设置等待超时时间)。

public boolean tryAcquire() {
    return tryAcquire(1, 0, MICROSECONDS);
  }

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;
  }

下面来分析一下:

  • mutex()
    获取锁的逻辑。通过双重校验锁的方式来保证锁的唯一性。
private Object mutex() {
    Object mutex = mutexDoNotUseDirectly;
    if (mutex == null) {
      synchronized (this) {
        mutex = mutexDoNotUseDirectly;
        if (mutex == null) {
          mutexDoNotUseDirectly = mutex = new Object();
        }
      }
    }
    return mutex;
  }
  • canAcquire
    是否获取token成功判断
private boolean canAcquire(long nowMicros, long timeoutMicros) {
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
  }
@Override
  final long queryEarliestAvailable(long nowMicros) {
    return nextFreeTicketMicros;
  }

queryEarliestAvailable这里返回的是下一个空闲的ticket产生的时间(毫秒),nowMicros是当前时间。
我们尝试把公式改一下,可能会好理解一些:

queryEarliestAvailable(nowMicros) <= nowMicros+timeoutMicros

这里的意思是,如果当前时间+等待的超时时间都比下一个空闲token产生的时间要早,则直接认为获取token失败,因为即使等待超时,也必然获取不到token。

  • reserveAndGetWaitLength
    更新并获取ticket,并sleep一定时间,返回给调用方。下面重点介绍这个方法。

五、更新ticket

/**
   * 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);
  }

@Override
  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;
  }
  • resync
    更新ticket的数量和时间
/** 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;
    }
  }

double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();

新增的ticket为 (当前时间-上一次最后更新时间)/coolDownIntervalMicros()
这里coolDownIntervalMicros如果选择的是SmoothBursty实现,则为每个ticket生成所需要的毫秒数。
eg.qps为20,则每个ticket生成所需要的时间为50ms。

storedPermits = min(maxPermits, storedPermits + newPermits);

更新存量的ticket,maxPermits前面已经提到,相当于桶的最大大小,默认是1s的qps数量。

  • storedPermitsToSpend和freshPermits
    storedPermitsToSpend:存量的ticket消费
    freshPermits:存量ticket不足,需要额外申请的ticket

long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);

等待时间计算,storedPermitsToWaitTime的SmoothBursty实现直接返回0,freshPermits * stableIntervalMicros为额外需要等待的毫秒数。


image.png

问题1:
这里的returnValue为什么不是直接返回nextFreeTicketMicros,而是直接获取nextFreeTicketMicros更新前的值?
问题2:
如果timeout设置为0,但是存量的ticket不足的情况下,需要freshPermits来填充,那么最终不是也会sleep吗?

五、总结

通过对RateLimiter的源码分析,对令牌桶算法的实现应该有一个比较深刻的理解了。上面提到的两个问题,下一篇我们再去分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容