限流之 Guava RateLimiter 实现原理浅析

Guava 作为一个工具包,内部提供了 RateLimiter 这样的限速工具,RateLimiter采用令牌桶的原理限制同一时刻的并发数量,起到限流作用。关于令牌桶的原理,如下图所示
在这里插入图片描述

  1. 令牌生成器负责向令牌桶内定时投递令牌,如果令牌桶满了,则溢出,保持令牌桶的令牌数不超过令牌桶容量

  2. 应用程序每次执行时,都要向令牌桶获取令牌,如果成功获取令牌,则继续执行,如果获取令牌失败,则拒绝执行

简单使用

  • 创建一个RateLimiter,设定每秒产生的令牌数量

    //每秒产生 10 个令牌
    RateLimiter rateLimiter = RateLimiter.create(10);
    
  • 阻塞获取令牌

    //从令牌桶内获取令牌,获取成功则返回,获取不到则一直阻塞直至获取
    rateLimiter.acquire();
    
  • 尝试获取令牌

    //尝试获取令牌,获取成功则立刻返回 true,否则返回 false
    boolean gainSuccess = rateLimiter.tryAcquire()
    

如下代码所示,展示了每间隔 1s 打印一次时间戳

RateLimiter rateLimiter = RateLimiter.create(1);

for (; ; ) {
    rateLimiter.acquire();
    System.out.println(System.currentTimeMillis());
}

打印结果

1578028136784
1578028137783
1578028138786
1578028139782
1578028140786
1578028141785
1578028142785
1578028143788
1578028144787
1578028145785
1578028146785

Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

RateLimiter是一个线程安全的对象,可以在多线程环境下使用

实现原理

在令牌桶模型中,有定时生成令牌的令牌生成器,而RateLimiter中并没有令牌生成器,也没有专门的后台线程来定时生成令牌,而是采用了基于时间戳、纯依赖使用方线程驱动的方式来实现。以下面代码为示例,创建一个每秒产生 10 个令牌的RateLimiter

//第 1 步
RateLimiter rateLimiter = RateLimiter.create(10);

for (; ; ) {

    // 第二步
    rateLimiter.acquire();

    //TODO Business
}

第一步的 RateLimiter 对象被创建后(实际上创建的是SmoothBursty对象,是RateLimiter的子类),主要有以下四个属性

属性类型描述
storedPermitsdouble令牌桶内可用的令牌数
stableIntervalMicrosdouble产生每个令牌的时间间隔,单位微妙
maxPermitsdouble令牌桶的容量,即能存放的令牌数量
nextFreeTicketMicroslong有令牌可用时的时间戳,在该时间戳前,RateLimiter都是无令牌可用的,相当于令牌资源的开放时间(注意,该时间戳是相对于RateLimit 创建时间的相对时间戳)

对应于上述的代码,则

  • stableIntervalMicros = 100 000.0,即 100 毫秒,平均 100 毫秒产生一个令牌
  • maxPermits = 10.0,即令牌桶的容量为 10 个令牌

在第二步通过 acquire()RateLimiter 获取令牌时,

  1. 如果当前时间戳大于nextFreeTicketMicros,将会拿当前时间戳减去nextFreeTicketMicros,然后除以 stableIntervalMicros ,计算出在 nextFreeTicketMicros当前时间戳之间的这段时间内应该产生的令牌数,然后存入storedPermits(即令牌桶)。就如同上面所说的纯依赖使用方线程驱动来产生令牌,这就是鲜明的体现。具体如下图所示(这里的当前时间戳并不是真正的当前时间戳,而是当前时间和RateLimiter创建时间之间的相对时间戳

    在这里插入图片描述

    计算这段时间内产生令牌数的公式如下

    ( nowMicros - nextFreeTicketMicros ) / stableIntervalMicros

  2. 如果storedPermits(桶内现有令牌数)小于请求的令牌数(示例中请求的令牌数是1),则用请求的令牌数减去storePermits表示还需要多少令牌,然后根据需要的令牌数乘以stableIntervalMicros,表示还需要多少时间可以产出需要的令牌数,最后在nextFreeTicketMicros 之上加上这个时间,即成为新的nextFreeTicketMicros,表示下一次有令牌可用的时间戳,如下图所示

    在这里插入图片描述

    如果storedPermits(桶内现有令牌数)大于或等于请求的令牌数(示例中请求的令牌数是1),则不重置nextFreeTicketMicros

上述只是讲述了关于令牌的管理,并未讲述到关于获取令牌,是因为RateLimiter采用了超前消费的处理方式,所谓的超前消费就是如果令牌桶内的令牌数小于一次要获取的令牌数,注意是一次,比如桶内有 1 个令牌,某次请求需要获取 10 个令牌,那么RateLimiter会将除了令牌桶内 1 个令牌外的其他 9 个未产生的令牌超前返回,并且在今后的 9 * stableIntervalMicros 的这段时间内不再提供令牌可用,在这段时间内所有获取令牌的请求都会阻塞,直至这段时间结束。RateLimiter超前消费就相当于拿未来生产令牌的时间来提前预支此次请求的差额令牌数,当然前提是当前请求时间戳大于RateLimiternextFreeTicketMicros,即当前RateLimiter有令牌可用。

弄懂了上面的概念,关于获取令牌是否阻塞、以及阻塞多久就一目了然了,获取令牌时,如果当前时间戳大于或者等于nextFreeTicketMicros,则不会阻塞,直接返回;如果当前时间戳小于nextFreeTicketMicros,则会阻塞等待到nextFreeTicketMicros,如下图所示

在这里插入图片描述

预热功能

作为一款限流器,RateLimiter不仅仅提供了限流的作用,还在限流的基础之上提供了预热的支持,所谓的预热就是考虑到应用在使用内存缓存场景下,应用在启动初期会根据请求来加载数据到内存,以供后续请求使用。对于这种情况,在初期不宜接受大量的请求,需要一个所谓的预热阶段,RateLimiter提供了对预热的支持,如下述API所示

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)

在预热阶段,RateLimiter并不会让请求快速到达permitsPerSecond设定的速率,而是在这段预热时间内逐步增长到permitsPerSecond设定的速率。

引用

谈谈服务限流算法的几种实现

Guava源码

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值