前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >经典限流算法设计与实现

经典限流算法设计与实现

作者头像
大忽悠爱学习
发布2023-10-27 16:54:46
3690
发布2023-10-27 16:54:46
举报
文章被收录于专栏:c++与qt学习
经典限流算法设计与实现

固定窗口限流算法

维护一个计数器,将单位时间段当做一个窗口,计数器记录该窗口接受请求的次数:

  • 当次数少于限流阈值,就允许访问,并且计数器+1
  • 当次数大于限流阈值,就拒绝访问
  • 当前的时间窗口过去之后,计数器清零

假设单位时间是一秒,限流阈值为3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阈值3,则后续的请求全部拒绝。等到1s结束后,计数器清零,重新开始计数。

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
public class FixedWindowRateLimiter implements RateLimiter{
    /**
     * 单位窗口时间内,最多可通信多少请求
     */
    private final int threshold = 3;
    /**
     * 窗口大小
     */
    private final long windowUnit = 1;
    /**
     * 当前窗口的初始创建时间
     */
    private long windowCreateTime;
    /**
     * 当前窗口已经通过的请求数量
     */
    private int counter;

    @Override
    public boolean tryAcquire() {
        // 1. 获取系统当前时间
        long currentTime = System.currentTimeMillis();
        // 2. 检查当前时间窗口是否过期
        if(currentTime - windowCreateTime > windowUnit){
            // 3. 重新开启一个时间窗口
            windowCreateTime = currentTime;
            counter = 0;
        }
        // 4. 判断当前窗口内放行的请求数量是否已经超过阈值
        if(counter > threshold){
            return false;
        }
        // 5. 放行请求
        counter++;
        return true;
    }
}

但是,这种算法有一个很明显的临界问题:假设限流阈值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阈值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阈值的定义了。

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

滑动窗口限流算法

滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。

一张图解释滑动窗口算法,如下:

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

假设单位时间还是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单位时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。然后呢,每个小周期,都有自己独立的计数器,如果请求是0.83s到达的,0.8~1.0s对应的计数器就会加1。

我们来看下滑动窗口是如何解决临界问题的?

假设我们1s内的限流阀值还是5个请求,0.8 ~ 1.0s内(比如0.9s的时候)来了5个请求,落在黄色格子里。时间过了1.0s这个点之后,又来5个请求,落在紫色格子里。如果是固定窗口算法,是不会被限流的,但是滑动窗口的话,每过一个小周期,它会右移一个小格。过了1.0s这个点后,会右移一小格,当前的单位时间段是0.2~1.2s,这个区域的请求已经超过限定的5了,已触发限流啦,实际上,紫色格子的请求都被拒绝啦。

TIPS: 当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

代码语言:javascript
复制
public class SlideWindowRateLimiter implements RateLimiter {
    /**
     * 循环队列,就是装多个窗口用,该数量是windowSize的2倍
     */
    private AtomicLong[] timeSlices;
    /**
     * 队列的总长度
     */
    private int timeSliceSize;
    /**
     * 每个时间片的时长,以毫秒为单位
     */
    private int timeMillisPerSlice;
    /**
     * 共有多少个时间片(即窗口长度)
     */
    private int windowSize;
    /**
     * 在一个完整窗口期内允许通过的最大阈值
     */
    private int threshold;
    /**
     * 该滑窗的起始创建时间,也就是第一个数据 (循环队列当前头元素对应时间戳)
     */
    private long beginTimestamp;
    /**
     * 最后一个数据的时间戳
     */
    private long lastAddTimestamp;

    public SlideWindowRateLimiter(int duration, int threshold) {
        //超过10分钟的按10分钟
        if (duration > 600) {
            duration = 600;
        }
        //要求5秒内探测出来的,
        if (duration <= 5) {
            this.windowSize = 5;
            this.timeMillisPerSlice = duration * 200;
        } else {
            this.windowSize = 10;
            this.timeMillisPerSlice = duration * 100;
        }
        this.threshold = threshold;
        // 保证存储在至少两个window
        this.timeSliceSize = windowSize * 2;

        reset();
    }

    @Override
    public boolean tryAcquire() {
        // 1. 当前自己所在的位置,是哪个小时间窗
        int index = locationIndex();
        // 2. 清空自己前面windowSize到2*windowSize之间的数据格的数据
        // 譬如1秒分4个窗口,那么数组共计8个窗口
        // 当前index为5时,就清空6、7、8、1。然后把2、3、4、5的加起来就是该窗口内的总和
        clearFromIndex(index);

        // 3. 在当前时间片里继续+1
        int sum = 0;
        sum += timeSlices[index].addAndGet(1);
        // 4. 加上前面几个时间片
        for (int i = 1; i < windowSize; i++) {
            sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
        }

        lastAddTimestamp = System.currentTimeMillis();

        // 5. 判断是否超出阈值
        return sum >= threshold;
    }

    /**
     * 初始化
     */
    private void reset() {
        beginTimestamp = System.currentTimeMillis();
        AtomicLong[] localTimeSlices = new AtomicLong[timeSliceSize];
        for (int i = 0; i < timeSliceSize; i++) {
            localTimeSlices[i] = new AtomicLong(0);
        }
        timeSlices = localTimeSlices;
    }

    /**
     * 计算当前所在的时间片的位置
     */
    private int locationIndex() {
        long now = System.currentTimeMillis();
        // 1. 如果当前的key已经超出一整个时间片了,那么就直接初始化就行了,不用去计算了
        if (now - lastAddTimestamp > (long) timeMillisPerSlice * windowSize) {
            reset();
        }
        // 2. 计算当前时间所在队列中的下标 (beginTimestamp表示当前循环队列头元素对应时间戳)
        int index = (int) (((now - beginTimestamp) / timeMillisPerSlice) % timeSliceSize);
        return Math.max(index, 0);
    }

    private void clearFromIndex(int index) {
        for (int i = 1; i <= windowSize; i++) {
            int j = index + i;
            if (j >= windowSize * 2) {
                j -= windowSize * 2;
            }
            timeSlices[j].set(0);
        }
    }
}

该段代码来源于JHotKey开源框架

滑动窗口算法虽然解决了固定窗口的临界问题,但是一旦到达限流后,请求都会直接暴力被拒绝。酱紫我们会损失一部分请求,这其实对于产品来说,并不太友好。


漏桶算法

漏桶算法面对限流,就更加的柔性,不存在直接的粗暴拒绝。

它的原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。

在这里插入图片描述
在这里插入图片描述
  • 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
  • 桶的容量一般表示系统所能处理的请求数。
  • 如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)
  • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。
代码语言:javascript
复制
public class LeakyBucketRateLimiter implements RateLimiter {
    /**
     * 每秒处理数(出水率)
     */
    private long rate;
    /**
     * 当前剩余水量
     */
    private long currentWater;
    /**
     * 最后刷新时间
     */
    private long refreshTime;
    /**
     * 桶容量
     */
    private long capacity;

    @Override
    public boolean tryAcquire() {
        // 1. 获取系统当前时间
        long currentTime = System.currentTimeMillis();
        // 2. 流出的水量 = (当前时间-上次刷新时间)* 出水率
        long outWater = (currentTime - refreshTime) / 1000 * rate;
        // 3. 当前水量 = 之前的桶内水量-流出的水量
        currentWater = Math.max(0, currentWater - outWater);
        // 4. 刷新时间
        refreshTime = currentTime; 

        // 5. 当前剩余水量还是小于桶的容量,则请求放行
        if (currentWater < capacity) {
            currentWater++;
            return true;
        }

        // 6. 当前剩余水量大于等于桶的容量,限流
        return false;
    }
}

在正常流量的时候,系统按照固定的速率处理请求,是我们想要的。但是面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,这就不是我们想看到的啦。流量变突发时,我们肯定希望系统尽量快点处理请求,提升用户体验嘛。


令牌桶算法

面对突发流量的时候,我们可以使用令牌桶算法限流。

令牌桶算法原理:

  1. 有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。
  2. 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
  3. 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;
  4. 如果拿不到令牌,就直接拒绝这个请求。
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
public class TokenBucketRateLimiter implements RateLimiter {
    /**
     * 每秒处理数(放入令牌数量)
     */
    private long putTokenRate;
    /**
     * 最后刷新时间
     */
    private long refreshTime;
    /**
     * 令牌桶容量
     */
    private long capacity;
    /**
     * 当前桶内令牌数
     */
    private long currentToken = 0L;

    @Override
    public boolean tryAcquire() {
        // 1. 获取系统当前时间
        long currentTime = System.currentTimeMillis();
        // 2. 生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
        long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate;
        // 3. 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
        currentToken = Math.min(capacity, generateToken + currentToken);
        // 4. 刷新时间
        refreshTime = currentTime;

        // 5. 桶里面还有令牌,请求正常处理
        if (currentToken > 0) {
            currentToken--;
            return true;
        }

        return false;
    }
}

如果令牌发放的策略正确,这个系统即不会被拖垮,也能提高机器的利用率。Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 经典限流算法设计与实现
  • 固定窗口限流算法
  • 滑动窗口限流算法
  • 漏桶算法
  • 令牌桶算法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档