Guava RateLimiter 原理 理解

背景

项目中限流可以用Guava RateLimiter,不想了解原理是啥吗?!
这篇文章已经写的很好了
没啥可补充了,加一些个人思考吧

思考

如果让你来造一个限流器,有啥想法?

直观想法1,对应参考文章的漏桶算法

就是用一个固定大小的队列。比如设置限流为5qps,1s可以接受5个请求;那我们就造一个大小为5的队列,如果队列为满了,就拒绝请求;如果队列未满,就往队列添加请求。

如何控制速率呢,我们通过控制消费者的消费速率是5qps,1s消费5个即可。

问题,说的挺轻巧,具体怎么控制消费者的速率呢?又加一个定时任务来消费队列吗,也挺费劲的。

想法2,对应参考文章的令牌算法

令牌听起来挺酷的。以固定的速率往桶里发放令牌。然后消费者每次要取到令牌(acquire)才可以响应请求

优点:由于令牌是固定间隔发放的,假设还是5qps,如果我有1s内没有请求,我的令牌桶就满了,可以一瞬间响应5个请求(一次过取5个令牌),也就是可以应对瞬时流量。

那么这里也涉及一个固定间隔发放的问题,难道也是需要定时任务往”桶里“放令牌吗?

那我们来看下Guava怎么搞的,先看下文章中的例子比较好

假设限流为2qps,那么固定发放令牌的时间stableIntervalMicros就是500ms,初始化的storedPermits当前桶里的令牌数是0。

在这里插入图片描述
在这里插入图片描述

操作说明requiredPermits请求多少令牌storedPermits桶里还有多少令牌nextFreeTicketMicros下一次能获得令牌的时间当前时间
初始化none000
直接获取10个令牌,直接预支付成功,获取成功1000 + 500 * 10 = 50000
按照上一步的计算,下次可以获得令牌的时间是5000ms,过了5000ms,sleep睡醒了,这时候再执行acquire(1)105000+500*1=55005000

debug看下分析对不对:核心代码:
在这里插入图片描述

第1次获取10个令牌

nowMicro是刚开始运行的时间,是一个很小的数,约等于0;
resync(nowMicro),更新令牌数,由于nowMicro约等于0,其实令牌数不会更新((0-0)/5000 = 0),令牌数还是0(约等于0)
storedPermitsToSpend,其实当前并没有令牌,所以取min,约等于0;
freshPermits,需要预支付10个令牌,约等于10;
预支付之后需要等待10*interval = 10 * 500 ,约等于5000ms,5000000微秒
this.nextFreeTicketMicros 需要加上 waitMicros 也就是 下一次可以获得令牌的时间是5000ms之后。

所以我们看到输出信息的第一行在第0s获取了10个令牌之后,下一次再想获取1个令牌需要等待5000ms也就是5s。

第2次获取1个令牌

然后再一次想获取1个令牌,当前时间还是约等于0,这时候resync,nowMicros(0)比nextFreeTicketMicros(5000)小,令牌不更新。returnValue=5000,storedPermitsToSpend=0,freshPermits=1,需要再等 waitMicros=1 * 500ms,然后nextFreeTicketMicros更新为5000+500=5500,返回returnValue=5000;外层函数睡眠5000ms,返回5000(输出打印获取1个token,约5s)

第3次获取10个令牌

上面说的,睡了5000ms,当前时间nowMicros=5000; resync,nowMicros(5000)比nextFreeTicketMicros(5500)小,令牌不更新,还是欠费状态,只能预支付。returnValue=5500, storedPermitsToSpend=0,freshPermits=10,需要预支付10个令牌, waitMicros=10 * 500ms = 5000,然后nextFreeTicketMicros更新为5500+5000=10500,返回returnValue=5500;外层函数睡眠5500-5000=500ms,返回500(输出打印获取10个token,约0.5s)

算法小结

所以咱们这个令牌算法的思路是怎样的呢?

1、对于给定的限流qps(如2),可以得到发放令牌的interval=1/2=0.5s,每0.5s发一个。

2、维护一个nextFreeTicketMicros,记录下一次可以获得令牌的时间;

3、每次来acquire请求,都会:

  • 先更新一下当前时间应该发放的令牌数(先发放令牌) :(当前时间-nextFreeTicketMicros)/ interval,同时不能大于桶的最大容量(这里是2);设置nextFreeTicketMicros=当前时间。
  • 然后计算一下当前桶里的令牌是不是够花(再扣除令牌)。允许预支。假设真有人奇怪地请求超多令牌 acquire(10),就会允许预支(10-2=8个令牌),然后计算在超支的情况下,waitMicros = 设定间隔 * 超支数 = 0.5 * 8 = 4s, 并将 nextFreeTicketMicros += waitMicros,然后sleep 一段时间(基本都是0),睡醒了再返回成功。

高并发的情况

高并发的情况下是怎么样的呢?

假设我们设置qps=2

我们会在每个请求里用tyrAcuiqre()来限流,每次只获取一个令牌,如果发现当前时间还没到nextFreeTicketMicro,那就说明无法获取,返回失败;否则的话会走上面那个核心代码,尝试去先发放令牌然后消耗令牌

tryAcquire代码:
在这里插入图片描述
原始状态令牌桶里有0个令牌,nextFreeTicketMicros = 0

然后一瞬间来了很多请求。

  • 第1个请求,更新令牌数量,nextFreeTicketMicros = 0,当前时间约等于0但是比0稍微大点,不会直接返回false,尝试走后面流程,先尝试发放令牌,当前时间只比0稍微大点,可以发放的令牌约等于0,令牌桶依然为0;然后扣除令牌,请求一个,欠费1个。等待时间(waitNanos=1*0.5=0.5s,nextFreeTicketMicros+=waitNanos=0.5s),返回值等于当前时间,所以睡眠时间=0,不会睡眠,直接返回成功;
  • 第2个请求,假设的是一瞬间来的多个请求,时间间隔约等于0,所以nextFreeTicketMicros=0.5s,大于当前时间,直接返回失败;
  • 后面多个请求,只要还没到0.5ms,都是因为小于nextFreeTicketMicros,返回失败;
  • 然后时间来到0.5s以后,来了一个请求,当前时间比nextFreeTicketMicros大了,开始走后面流程,发放令牌:由于时间差太小了,实际上还是没有增加令牌,令牌桶为空,更新nextFreeTicketMicros=当前时间;扣除令牌:请求一个,令牌桶是空的,欠费1个。等待时间(waitNanos=1*0.5=0.5s,nextFreeTicketMicros+=waitNanos=1.0s),返回值等于当前时间,所以睡眠时间=0,不会睡眠,直接返回成功;

做个实验吧,看看对不对:

    @Test
    public void testSmoothBursty() throws InterruptedException {
        RateLimiter r = RateLimiter.create(2);
        Long time = System.currentTimeMillis();
        while (true) {
            boolean b = r.tryAcquire();
            if (b) {
                Long diff = System.currentTimeMillis() - time;
                time = System.currentTimeMillis();
                System.out.println("get 1 tokens: " +  diff);
            }
//            Thread.sleep(100);
        }
    }

输出结果:
在这里插入图片描述
符合预期
第一个请求约等于没有睡眠
后面的请求只有每过500ms的请求返回成功

什么时候会sleep?

我们发现,底层是会调用这个函数的:
reserveAndGetWaitLength
在这里插入图片描述
将返回值和当前时间相减,得到的值将用于睡眠。
在这里插入图片描述
所以什么时候会睡眠呢?我们仔细分析下代码

在这里插入图片描述
返回值就是,调用发放令牌之后的nextFreeTicketMicros
仔细看怎么发放令牌:
在这里插入图片描述
如果当前时间大于nextFreeTicketMicros,那就会更新nextFreeTicketMicros=当前时间。

所以,如果当前时间大于nextFreeTicketMicros,更新nextFreeTicketMicros=当前时间,再返回,然后用这个返回值将去当前时间,这么一看,那肯定等于0,不用睡;也就是说,如果当前时间大于nextFreeTicketMicros,不用睡。

所以什么时候要睡呢,那就是当前时间 【不】大于nextFreeTicketMicros呗。什么时候当前时间 【不】大于nextFreeTicketMicros呢?想象这么一个场景:我们在调用acquire而不是tryAcquire的时候,比如acquire(10000),申请超级多的令牌,由于我们支持超支,waitNanos=10000*interval=超多时间,于是就把nextFreeTicketMicros更新成远远超过当前时间了,这时候我们就会发现当前时间 【不】大于nextFreeTicketMicros,需要睡超级久才行。。

综上,当我们使用acquire()而不是tryAcquire的时候,所有请求都会排队,sleep到自己该执行的时间,然后执行。

实验一把:

    public void acquireThread(RateLimiter r) {
        double acquire = r.acquire();
        System.out.println("get 1 tokens: " +  acquire);
    }

    @Test
    public void testAcquire() throws InterruptedException {
        RateLimiter r = RateLimiter.create(2);
        for (int i = 0; i < 20; i++) {
            new Thread(() -> acquireThread(r)).start();
            //            double acquire = r.acquire();
//            System.out.println("get 1 tokens: " +  acquire);
        }
        System.out.println("loop end already!!");
        Thread.sleep(10000);
    }

在这里插入图片描述
可以看见,循环启动子线程之后,就跳出循环了,但是这些子线程实际上得慢慢排队,等到属于他们的时刻才可以运行。在这种情况下,虽然我们没有用到队列,却貌似申请实现了队列的效果。

核心原理是啥?实际上是通过锁的机制,抢着去设置nextFreeTicketMicros,nextFreeTicketMicros每次递增interval,每个线程成功设置到nextFreeTicketMicros的话,就会知道自己要睡多久。

具体:初始化nextFreeTicketMicros=0;同时来了20个线程

  • 第一个抢到的线程,now约等于0,将nextFreeTicketMicros设置成now,约等于0;扣除令牌:请求1个,发现欠费,nextFreeTicketMicros+=waitMicro(interval*1)=0+0.5=0.5; 将nextFreeTicketMicros设置成0.5,不需要睡;
  • 第二个抢到的线程,now约等于0,nextFreeTicketMicros大于now,不会设置将nextFreeTicketMicros(等于上一次请求的0.5,用于返回);扣除令牌:请求1个,发现欠费,nextFreeTicketMicros+=waitMicro(interval*1)=0.5+0.5=1; 将nextFreeTicketMicros设置成1,需要睡0.5-now(约等于0)=0.5;
    • 第三个抢到的线程,now约等于0,nextFreeTicketMicros大于now,不会设置将nextFreeTicketMicros(等于上一次请求的1,用于返回);扣除令牌:请求1个,发现欠费,nextFreeTicketMicros+=waitMicro(interval*1)=1+0.5=1.5; 将nextFreeTicketMicros设置成1.5,需要睡1-now(约等于0)=1;

小结

再难啃的代码,一行一行debug,用脑子当计算机过几遍,总会有一点理解的,不要害怕不要虚,奥利给!

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值