Guava
作为一个工具包,内部提供了
RateLimiter
这样的限速工具,
RateLimiter
采用令牌桶的原理限制同一时刻的并发数量,起到限流作用。关于令牌桶的原理,如下图所示
-
令牌生成器负责向令牌桶内定时投递令牌,如果令牌桶满了,则溢出,保持令牌桶的令牌数不超过令牌桶容量
-
应用程序每次执行时,都要向令牌桶获取令牌,如果成功获取令牌,则继续执行,如果获取令牌失败,则拒绝执行
-
创建一个
RateLimiter
,设定每秒产生的令牌数量
RateLimiter rateLimiter = RateLimiter.create(10);
-
阻塞获取令牌
rateLimiter.acquire();
-
尝试获取令牌
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
RateLimiter rateLimiter = RateLimiter.create(10);
for (; ; ) {
rateLimiter.acquire();
第一步的 RateLimiter
对象被创建后(实际上创建的是SmoothBursty
对象,是RateLimiter
的子类),主要有以下四个属性
属性 | 类型 | 描述 |
---|
storedPermits | double | 令牌桶内可用的令牌数 |
stableIntervalMicros | double | 产生每个令牌的时间间隔,单位微妙 |
maxPermits | double | 令牌桶的容量,即能存放的令牌数量 |
nextFreeTicketMicros | long | 有令牌可用时的时间戳,在该时间戳前,RateLimiter 都是无令牌可用的,相当于令牌资源的开放时间(注意,该时间戳是相对于RateLimit 创建时间的相对时间戳) |
对应于上述的代码,则
stableIntervalMicros = 100 000.0
,即 100 毫秒,平均 100 毫秒产生一个令牌maxPermits = 10.0
,即令牌桶的容量为 10 个令牌
在第二步通过 acquire()
向 RateLimiter
获取令牌时,
-
如果当前时间戳大于nextFreeTicketMicros
,将会拿当前时间戳减去nextFreeTicketMicros
,然后除以 stableIntervalMicros
,计算出在 nextFreeTicketMicros
到当前时间戳之间的这段时间内应该产生的令牌数,然后存入storedPermits
(即令牌桶)。就如同上面所说的纯依赖使用方线程驱动来产生令牌,这就是鲜明的体现。具体如下图所示(这里的当前时间戳并不是真正的当前时间戳,而是当前时间和RateLimiter
创建时间之间的相对时间戳)
计算这段时间内产生令牌数的公式如下
( nowMicros - nextFreeTicketMicros ) / stableIntervalMicros
-
如果storedPermits
(桶内现有令牌数)小于请求的令牌数(示例中请求的令牌数是1),则用请求的令牌数减去storePermits
表示还需要多少令牌,然后根据需要的令牌数乘以stableIntervalMicros
,表示还需要多少时间可以产出需要的令牌数,最后在nextFreeTicketMicros
之上加上这个时间,即成为新的nextFreeTicketMicros
,表示下一次有令牌可用的时间戳,如下图所示
如果storedPermits
(桶内现有令牌数)大于或等于请求的令牌数(示例中请求的令牌数是1),则不重置nextFreeTicketMicros
上述只是讲述了关于令牌的管理,并未讲述到关于获取令牌,是因为RateLimiter
采用了超前消费的处理方式,所谓的超前消费就是如果令牌桶内的令牌数小于一次要获取的令牌数,注意是一次,比如桶内有 1 个令牌,某次请求需要获取 10 个令牌,那么RateLimiter
会将除了令牌桶内 1 个令牌外的其他 9 个未产生的令牌超前返回,并且在今后的 9 * stableIntervalMicros
的这段时间内不再提供令牌可用,在这段时间内所有获取令牌的请求都会阻塞,直至这段时间结束。RateLimiter
的超前消费就相当于拿未来生产令牌的时间来提前预支此次请求的差额令牌数,当然前提是当前请求时间戳大于RateLimiter
的nextFreeTicketMicros
,即当前RateLimiter
有令牌可用。
弄懂了上面的概念,关于获取令牌是否阻塞、以及阻塞多久就一目了然了,获取令牌时,如果当前时间戳大于或者等于nextFreeTicketMicros
,则不会阻塞,直接返回;如果当前时间戳小于nextFreeTicketMicros
,则会阻塞等待到nextFreeTicketMicros
,如下图所示
作为一款限流器,RateLimiter
不仅仅提供了限流的作用,还在限流的基础之上提供了预热的支持,所谓的预热就是考虑到应用在使用内存缓存场景下,应用在启动初期会根据请求来加载数据到内存,以供后续请求使用。对于这种情况,在初期不宜接受大量的请求,需要一个所谓的预热阶段,RateLimiter
提供了对预热的支持,如下述API所示
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
在预热阶段,RateLimiter
并不会让请求快速到达permitsPerSecond
设定的速率,而是在这段预热时间内逐步增长到permitsPerSecond
设定的速率。
谈谈服务限流算法的几种实现
Guava源码
Guava提供的RateLimiter可以限制物理或逻辑资源的被访问速率,咋一听有点像java并发包下的Samephore,但是又不相同,RateLimiter控制的是速率,Samephore控制的是并发量。RateLimiter的原理类似于令牌桶,它主要由许可发出的速率来定义,如果没有额外的配置,许可证将按每秒许可证规定的固定速度分配,许可将被平滑地分发,若请求超过permitsPerSecon...
浏览器打开
一、问题描述
某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机...
浏览器打开
1. RateLimiter基于令牌桶算法,即以用户设定的恒定速率向令牌桶内放置令牌,用户来执行任务时,只有拿到令牌才能执行;
2. RateLimiter对于持续生成令牌,采用的不是定时任务的方式(过于耗费资源,不适合高并发),而是使用延迟计算的方式,即在获取令牌时计算上一次时间nextFreeTicketMicros和当前时间之间的差值,计算这段时间之内按照用户设定的速率可以生产多少令牌;
void resync(long nowMicros) {
// if nextFreeTic...
浏览器打开
一、问题描述
某天A君突然发现自己的接口请求量突然涨到之前的10倍,没多久该接口几乎不可使用,并引发连锁反应导致整个系统崩溃。如何应对这种情况呢?生活给了我们答案:比如老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引
浏览器打开
如果让你来造一个限流器,有啥想法?
直观想法1,对应参考文章的漏桶算法
就是用一个固定大小的队列。比如设置限流为5qps,1s可以接受5个请求;那我们就造一个大小为5的队列,如果队列为满了,就拒绝请求;如果队列未满,就往队列添加请求。
如何控制速率呢,我们通过控制消费者的消费速率是5qps,1s消费5个即可。
问题,说的挺轻巧,具体怎么控制消费者的速率呢?又加一个定
浏览器打开