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

经典限流算法设计与实现

作者头像
大忽悠爱学习
发布于 2023-10-27 08:54:46
发布于 2023-10-27 08:54:46
66200
代码可运行
举报
文章被收录于专栏:c++与qt学习c++与qt学习
运行总次数:0
代码可运行
经典限流算法设计与实现

固定窗口限流算法

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
趣解设计模式之《小店儿菜单的故事》
在一座小镇上,有两家特别有名气的小店,一家是远近闻名的早餐店,它家的早餐特别好吃,每天早上都能排起长长的队伍;另一家是个蛋糕店,他家是专门从法国请来的蛋糕师傅,蛋糕的香味真是香飘万里。
爪哇缪斯
2023/09/14
1840
趣解设计模式之《小店儿菜单的故事》
用C# (.NET Core) 实现迭代器设计模式
本文的概念来自深入浅出设计模式一书 项目需求 有两个饭店合并了, 它们各自有自己的菜单. 饭店合并之后要保留这两份菜单. 这两个菜单是这样的: 菜单项MenuItem的代码是这样的: 最初我们是这样设
solenovex
2018/04/28
7920
用C# (.NET Core) 实现迭代器设计模式
【万字图文】详解设计模式(下篇)
本篇关于设计模式的文章是设计模式的下篇,上下两篇文章介绍了整个23种设计模式中的22种,由于解释器模式主要应用场景是在解释器开发中,所以,该模式就只列举出了一些含义和使用场景,并没有类图和示例。而其他的22种设计模式,基本遵循的是:待解决的问题——>应用场景——>模式定义——>类图——>具体实现——>优缺点,这几方面来介绍的。那么,话不多说了,还是老样子,如下为本篇文章的整体目录:
爪哇缪斯
2023/05/10
1.2K0
【万字图文】详解设计模式(下篇)
常用设计模式——迭代器模式
迭代器模式 概念 提供一种方法顺序访问一个集合中的各个元素,而又不暴露其内部实现。 示例 演示了迭代器模式,遍历餐厅菜单的例子 /** * 菜单项 * @author huangy on 2019
用户5325874
2020/01/16
5780
常用设计模式——迭代器模式
迭代器模式
有两家餐厅,共有两个菜单,两家准备合并,每个餐厅都有一个销售系统,但是内部菜单的组成结构稍有不同,一个是用数组存储菜单对象,一个是用集合存储菜单对象
杨小杰
2019/07/04
5600
迭代器模式
Head First设计模式——迭代器模式
前言:迭代器模式平时用的不多,因为不管C#还是Java都已经帮我封装了,但是你是否知道平时经常在用的东西本质是怎么回事呢。
SpringSun
2020/08/11
3570
Head First设计模式——迭代器模式
设计模式之迭代器与组合模式(二)
在上次的文章中,我们通过层层引导,已经知道了迭代器模式的由来。现在我们再好好总结下。
程序员小跃
2019/12/25
4640
组合模式.
组合部件为叶子节点和组合节点定义了统一的接口。所有的操作,如果子类没有实现,我们默认抛出一个 UnsupportedOperationException 异常。
JMCui
2019/01/03
8050
设计模式之迭代器与组合模式(四)
我们要如何在菜单上应用组合模式呢?一开始,我们需要创建一个组件接口来作为菜单和菜单项的共同接口,让我们能够用统一的做法来处理菜单和菜单项。换句话说,我们可以针对菜单或菜单项调用相同的方法。
程序员小跃
2019/12/27
3400
设计模式之迭代器与组合模式(四)
设计之禅——迭代器模式
迭代器想必大家不会陌生,作为Java中内置的API,平时我们使用的也是非常多的。但你是否曾想过它和迭代器模式有什么关联?并且Java中已经有for循环遍历,为什么还会需要这样一个类?
夜勿语
2020/09/07
2930
使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)
本文的概念性内容来自深入浅出设计模式一书. 本文需结合上一篇文章(使用C# (.NET Core) 实现迭代器设计模式)一起看. 上一篇文章我们研究了多个菜单一起使用的问题. 需求变更 就当我们感觉我
solenovex
2018/05/30
1.1K0
设计模式之组合模式
组合模式:将对象聚合成树形结构来表现“整体/部分”的层次结构。 组合模式能让客户以一致的方式来处理个别对象以及对象组合。 也就是我们可以忽略对象组合与个体对象之间的差别
用户9854323
2022/06/25
1970
设计模式之组合模式
设计模式----迭代器模式
迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部表现。 示例:餐厅和煎饼屋合并后,需要遍历菜单时,由于餐厅菜单使用的是数组,而煎饼屋使用的是ArrayList,怎么才能统一地用一种方法来遍历呢? 由于Java内置对迭代器的支持,我们利用javautil.Iterator包来实现。 首先看一下iterator接口: <<interface>>iterator hasNext() next() remove() hasNext() next() remove
SuperHeroes
2018/05/30
4820
设计模式之迭代器模式 引导篇
这两天,比较火的并购新闻就是,网易考拉被阿里以20亿美元收购。从此网易考拉不再姓“网”而姓“阿”了。并购后的网易考拉和阿里的电商系统进行对接。那么问题来了:在阿里有个早餐店的菜单(CakeHouseMenu)使用的事ArrayList来存放菜单的,考拉有个午餐店的菜单(DinerMenu)使用的是数组结构存放的。现在考拉和阿里合并了,两个点的菜单也要合并。
凯哥Java
2019/09/07
4800
设计模式之迭代器模式 引导篇
HeadFirst设计模式
抽象父类只负责实现相应功能不用管具体是哪一个实现。 子类想调用哪一个行为,就new一个新的行为。
Tim在路上
2020/08/04
3650
Head First设计模式——组合模式
  组合模式的例子我们继续延续上篇《Head First设计模式——迭代器模式》的菜单例子,首先声明下迭代器和组合模式没有什么关系,他们是两个不同模式。只是我们在这个菜单例子的组合模式内部会用到迭代器。
SpringSun
2020/08/11
4770
Head First设计模式——组合模式
设计模式-迭代器模式
迭代器模式(Iterator Pattern)是一种行为型设计模式,它可以让我们在不暴露对象实现细节的情况下,访问一个聚合对象中的每一个元素,而无需暴露其内部结构。迭代器模式通过将聚合对象的遍历操作委托给一个迭代器对象来实现这一目标。迭代器模式提供了一种标准的遍历方式,使得我们可以在不改变聚合对象代码的情况下,实现对聚合对象的不同遍历方式。
堕落飞鸟
2023/05/04
1630
设计模式之迭代器与组合模式(一)
很高兴,这本书总共13章,这次已经是到第9章了;同时也很遗憾,小编脱离了书本,还是不知道如何描述一个设计模式。就比如迭代器与组合模式,原书篇幅比较长,小编尽量通俗易懂些,不到之处,还请各位小伙伴参考原书,小编也欢迎和大家一起交流。
程序员小跃
2019/12/26
4330
设计模式——迭代器模式
设计模式——迭代器模式
Java架构师必看
2021/05/14
3710
设计模式——迭代器模式
图解Java设计模式之迭代器模式
编写程序展示一个学校院系结构 :需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。如图 :
海仔
2020/04/01
4440
图解Java设计模式之迭代器模式
相关推荐
趣解设计模式之《小店儿菜单的故事》
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验