常见的四种限流算法及基础实现

常见的四种限流算法及基础实现

  • 什么是限流
  • 有哪些限流算法?
  • 限流算法
    • 固定窗口
    • 滑动窗口
    • 漏桶算法
    • 令牌算法

什么是限流

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。

在高并发系统中,出于系统保护角度考虑,通常会对流量进行限流。

在分布式系统中,高并发场景下,为了防止系统因突然的流量激增而导致的崩溃,同时保证服务的高可用性和稳定性,限流是最常用的手段。

有哪些限流算法?

常见的四种限流算法,分别是:固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法。

限流算法

固定窗口

1. 实现原理

  • 固定窗口又称固定窗口(又称计数器算法,Fixed Window)限流算法,是最简单的限流算法。

  • 实现原理:在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。如图所示,我们要求 3 秒内的请求不要超过 150 次:

请添加图片描述

2. 简易代码实现

public class FixedWindow {

    private long time = new Date().getTime();

    private Integer count = 0; // 计数器
    private final Integer max = 100; // 请求阈值
    private final Integer interval = 1000; // 窗口大小

    public boolean trafficMonitoring() {
        long nowTime = new Date().getTime();
        if (nowTime < time + interval) {
            // 在时间窗口内
            count++;
            return max > count;
        } else {
            time = nowTime; // 开启新的窗口
            count = 1; // 初始化计数器,由于这个请求属于当前新开的窗口,所以记录这个请求
            return true;
        }
    }
}

3. 优缺点

  1. 优点:实现简单,容易理解
  2. 缺点:
  • 限流不够平滑。例如:限流是每秒 3 个,在第一毫秒发送了 3 个请求,达到限流,窗口剩余时间的请求都将会被拒绝,体验不好。

  • 无法处理窗口边界问题。因为是在某个时间窗口内进行流量控制,所以可能会出现窗口边界效应,即在时间窗口的边界处可能会有大量的请求被允许通过,从而导致突发流量

即:如果第 2 到 3 秒内产生了 150 次请求,而第 3 到 4 秒内产生了 150 次请求,那么其实在第 2 秒到第 4
秒这两秒内,就已经发生了 300 次请求了,远远大于我们要求的 3 秒内的请求不要超过 150 次这个限制,如下图所示:

请添加图片描述

滑动窗口

1. 实现原理

  • 滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求。在滑动窗口算法中,窗口的起止时间是动态的,窗口的大小固定。这种算法能够较好地处理窗口边界问题,但是实现相对复杂,需要记录每个请求的时间戳。

  • 实现原理:滑动窗口在固定窗口的基础上,将时间窗口进行了更精细的分片,将一个窗口分为若干个等份的小窗口,每次仅滑动一小块的时间。每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平

  • 移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阈值。其中,Sentinel 就是采用滑动窗口算法来实现限流的

如图所示:

在这里插入图片描述
2. 使用zset实现滑动窗口

通过使用springAOP和redis中的zset进行实现滑动窗口限流

定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {

    /**
     * 限流时间,单位秒
     */
    int time() default 5;

    /**
     * 限流次数
     */
    int count() default 10;
}

对应注解限流处理切面(主要)

@Aspect
@Component
public class RateLimiterAspect {

    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 实现限流(新思路)
     * @param point
     * @param rateLimiter
     * @throws Throwable
     */
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        // 在 {time} 秒内仅允许访问 {count} 次。
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        // 根据用户IP(可选)和接口方法,构造key
        String combineKey = getCombineKey(point);
        System.err.println(combineKey);
        // 记录本次访问的时间结点
        long currentMs = System.currentTimeMillis();
        redisTemplate.opsForZSet().add(combineKey, String.valueOf(currentMs), currentMs);
        // 这一步是为了防止一直存在于内存中
        redisTemplate.expire(combineKey, time, TimeUnit.SECONDS);
        // 移除{time}秒之前的访问记录(滑动窗口思想)
        redisTemplate.opsForZSet().removeRangeByScore(combineKey, 0, currentMs - time * 1000);

        // 获得当前窗口内的访问记录数
        Long currCount = redisTemplate.opsForZSet().zCard(combineKey);
        // 限流判断
        if (currCount !=null && currCount > count) {
            //返回异常提示
            log.error("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, currCount, combineKey);
            //todo 返回异常

        }
    }
    /**
     * @param point 切入点
     * @return 组合key
     */
    private String getCombineKey(JoinPoint point) {
        StringBuilder sb = new StringBuilder("rate_limit:");

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // keyPrefix + "-" + class + "-" + method  //类名加方法名
        return sb.append("-").append( targetClass.getName() )
                .append("-").append(method.getName()).toString();
    }
}

使用@RateLimiter注解进行限流

    @RateLimiter(time = 1, count = 10)
    @GetMapping("/testIndex")
    public String index(){
        System.out.println("处理请求");
        return "finish";
    }

3. 优缺点

  1. 优点:解决了固定窗口算法的窗口边界问题,避免突发流量压垮服务器。
  2. 缺点:还是存在限流不够平滑的问题。例如:限流是每秒 3 个,在第一毫秒发送了 3 个请求,达到限流,剩余窗口时间的请求都将会被拒绝,体验不好。

漏桶算法

1. 实现原理
漏桶限流算法是一种常用的流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制数据的传输速率以及防止网络拥塞。

主要的作用:

  • 控制数据注入网络的速度。
  • 平滑网络上的突发流量

实现原理:
漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。不管上面的水流速度有多块,漏桶水滴的流出速度始终保持不变。消息中间件就采用的漏桶限流的思想

如图所示:
请添加图片描述
核心步骤:

  1. 一个固定容量的漏桶,按照固定速率出水(处理请求);
  2. 当流入水(请求数量)的速度过大会直接溢出(请求数量超过限制则直接拒绝)。
  3. 桶里的水(请求)不够则无法出水(桶内没有请求则不处理)。

2. 简易实现

public class LeakyBucket {
    private long capacity;  // 漏桶容量
    private long rate;      // 流出速率
    private long water;     // 当前水量
    private long lastTime;  // 上次请求时间

    public LeakyBucket(long capacity, long rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastTime = System.currentTimeMillis();
    }

    public synchronized boolean allow() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastTime;
        lastTime = now;

        // 先漏水,根据流出速率计算漏掉的水量
        water = Math.max(0, water - elapsedTime * rate);

        // 检查水量是否超出了容量
        if (water < capacity) {
            water++;
            return true;  // 请求通过,水量增加
        } else {
            return false;  // 请求被拒绝,水量已满
        }
    }
    
}

3. 优缺点

  1. 优点:
  • 平滑流量。由于漏桶算法以固定的速率处理请求,可以有效地平滑和整形流量,避免流量的突发和波动(类似于消息队列的削峰填谷的作用)。
  • 防止过载。当流入的请求超过桶的容量时,可以直接丢弃请求,防止系统过载。
  1. 缺点:
  • 无法处理突发流量:由于漏桶的出口速度是固定的,无法处理突发流量。例如,即使在流量较小的时候,也无法以更快的速度处理请求。
  • 可能会丢失数据:如果入口流量过大,超过了桶的容量,那么就需要丢弃部分请求。在一些不能接受丢失请求的场景中,这可能是一个问题。
  • 不适合速率变化大的场景:如果速率变化大,或者需要动态调整速率,那么漏桶算法就无法满足需求。
  • 资源利用率:不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。

由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。

令牌算法

1. 实现原理
令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。

实现原理:

  1. 系统以固定的速率向桶中添加令牌;
  2. 当有请求到来时,会尝试从桶中移除一个令牌,如果桶中有足够的令牌,则请求可以被处理或数据包可以被发送;
  3. 如果桶中没有令牌,那么请求将被拒绝;
  4. 桶中的令牌数不能超过桶的容量,如果新生成的令牌超过了桶的容量,那么新的令牌会被丢弃。

令牌桶算法的一个重要特性是,它能够应对突发流量。当桶中有足够的令牌时,可以一次性处理多个请求,这对于需要处理突发流量的应用场景非常有用。但是又不会无限制的增加处理速率导致压垮服务器,因为桶内令牌数量是有限制的。

在这里插入图片描述
2. 代码实现
Guava 中的 RateLimiter 就是基于令牌桶实现的,可以直接拿来使用。

<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
    public void acquireTest() {
        //每秒固定生成5个令牌
        RateLimiter rateLimiter = RateLimiter.create(5);
        for (int i = 0; i < 10; i++) {
            double time = rateLimiter.acquire();
            logger.info("等待时间:{}s", time);
        }
    }

3. 优缺点

  1. 优点
  • 可以处理突发流量:令牌桶算法可以处理突发流量。当桶满时,能够以最大速度处理请求。这对于需要处理突发流量的应用场景非常有用。
  • 限制平均速率:在长期运行中,数据的传输率会被限制在预定义的平均速率(即生成令牌的速率)。
  • 灵活性:与漏桶算法相比,令牌桶算法提供了更大的灵活性。例如,可以动态地调整生成令牌的速率。
  1. 缺点:
  • 可能导致过载:如果令牌产生的速度过快,可能会导致大量的突发流量,这可能会使网络或服务过载。
  • 需要存储空间:令牌桶需要一定的存储空间来保存令牌,可能会导致内存资源的浪费。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/515777.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+uniapp的同城活动报名系统开发找搭子软件

项目背景 随着移动互联网的飞速发展&#xff0c;人们的社交方式也在不断变化。在这个大背景下&#xff0c;同城活动报名系统应运而生&#xff0c;成为了连接人与人、活动与人之间的桥梁&#xff0c;深受广大年轻人的喜爱。在这个充满机遇与挑战的时代&#xff0c;同城活动报名…

GDPU 竞赛技能实践 天码行空6

&#x1f4d6; 敌兵布阵 C国的死对头A国这段时间正在进行军事演习&#xff0c;所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段&#xff0c;所以每个工…

Flume 拦截器概念及自定义拦截器的运用

文章目录 Flume 拦截器拦截器的作用拦截器运用1.创建项目2.实现拦截器接口3.编写事件处理逻辑4.拦截器构建5.打包与上传6.编写配置文件7.测试运行 Flume 拦截器 在 Flume 中&#xff0c;拦截器&#xff08;Interceptors&#xff09;是一种可以在事件传输过程中拦截、处理和修改…

Linux网络协议栈从应用层到内核层④

文章目录 1、网卡接受数据2、网络设备层接收数据3、ip层接受数据4、tcp层接受数据5、上层应用读取数据6、数据从网卡到应用层的整体流程 1、网卡接受数据 当网卡收到数据时&#xff0c;会触发一个中断&#xff0c;然后就会调用对应的中断处理函数&#xff0c;再做进一步处理。…

python相对路径导包与绝对路径导包的正确方式

【python相对路径导包与绝对路径导包的正确方式】 python相对路径导包与绝对路径导包的正确方式_哔哩哔哩_bilibilipython导包的难题&#xff0c;今天解决了&#xff0c;相对路径导包和绝对路径导包&#xff0c;均可以&#xff01;&#xff01;&#xff01;, 视频播放量 5、弹…

如何(关闭)断开 Websocket 连接:简单易懂的实现指南

WebSocket 协议提供了一条用于 Web 应用程序中双向通讯的高效通道&#xff0c;让服务器能够实时地向客户端发送信息&#xff0c;而无需客户端每次都发起请求。本文旨在探讨有关结束 WebSocket 连接的适当时机&#xff0c;内容包括协议的基础知识、如何结束连接、一些使用场景&a…

maven本地仓库设置

1、背景 我们在本地安装好maven后&#xff0c;java环境也安装好了以后&#xff0c;运行java项目A,我希望把项目A所有的依赖安装在我电脑中的a文件夹下&#xff0c;项目B安装在我电脑的b文件夹下。 2、解决 需要在 maven 文件中找到 conf 文件夹下的 settings.xml 文件进行修…

Unity | Shader基础知识(第十一集:什么是Normal Map法线贴图)

目录 前言 一、图片是否有法线贴图的视觉区别 二、有视觉区别的原因 三、法线贴图的作用 四、信息是如何存进去的 五、自己写一个Shader用到法线贴图 六、注意事项 七、作者的话 前言 本小节会给大家解释&#xff0c;什么是法线贴图&#xff1f;为什么法线贴图会产生深…

SpringBoot -- 外部化配置

我们如果要对普通程序的jar包更改配置&#xff0c;那么我们需要对jar包解压&#xff0c;并在其中的配置文件中更改配置参数&#xff0c;然后再打包并重新运行。可以看到过程比较繁琐&#xff0c;SpringBoot也注意到了这个问题&#xff0c;其可以通过外部配置文件更新配置。 我…

前端三剑客 —— CSS (上)

上节内容中提到了 前端三剑客 —— HTML 超文本标记语言&#xff0c;这节内容 跟大家讲述三剑客中的第二个 CSS。 CSS 什么是CSS Cascading Style Sheel&#xff0c;简称CSS&#xff0c;中文叫层叠样式表&#xff0c;也叫级联样式表。主要作用是来修饰HTML页面的一种技术。 …

【C++学习】哈希表的底层实现及其在unordered_set与unordered_map中的封装

文章目录 1. unordered系列关联式容器1.1 unordered_map1.2 unordered_set1.3.底层结构 2.哈希2.1哈希概念2.2哈希冲突2.3 哈希函数2.4 哈希冲突解决2.4.1闭散列2.4.1开散列2.5开散列与闭散列比较 3.哈希的模拟实现1. 模板参数列表2. 迭代器的实现3. 增加通过key获取value操作4…

66toolkit终极网络工具系统:470+强大Web工具,助力您的网络运营与开发

一、产品介绍 66toolkit&#xff0c;被誉为“终极网络工具系统”&#xff08;SAAS&#xff09;&#xff0c;是一款功能强大的PHP脚本。它集合了超过470种快速且易用的Web工具&#xff0c;为日常任务处理和开发人员提供了极大的便利。作为一款综合性的网络工具系统&#xff0c;…

面试题目--fork

问题&#xff1a; (1)fork 以后&#xff0c;父进程打开的文件指针位置在子进程里面是否一样&#xff1f;(先open再fork) (2)能否用代码简单的验证一下? (3)先fork再打开文件父子进程是否共享偏移量?父进程打开的文件指针位置在子进程里面是否一样&#xff1f;能否用代码简…

武汉星起航:引领亚马逊孵化新篇章,助力合作伙伴共创商业辉煌

武汉星起航电子商务有限公司自2020年成立以来&#xff0c;凭借其敏锐的市场洞察和深度合作模式&#xff0c;在跨境电商领域取得了显著的成绩。为了进一步满足市场需求&#xff0c;公司决定推出亚马逊一站式孵化平台&#xff0c;为合作伙伴提供更全面的指导和支持。 该孵化平台…

【办公类-47-01】20240404 Word内部照片批量缩小长宽(课题资料系列)

作品展示 背景需求 最近在做《运用Python优化3-6岁幼儿学习操作材料的实践研究》的课题研究资料&#xff08;上半学期和下半学期&#xff09;。 将CSDN里面相关的研究照片文字贴入Word后&#xff0c;就发现一张图片就占了A4竖版一页&#xff0c;太大了。我想把word里面的所有…

数学结论在dsa中的应用

1. LC 3102 最小化曼哈顿距离 VP周赛391 T4。这是个结论题目。 首先曼哈顿距离是需要两个数对而不是两个数去进行比较的&#xff0c;两个数之间你很轻易就知道差的绝对值最大是多少了&#xff0c;只要挑最大和最小两个数一减就可以了。 但是两个数对之间各项差的绝对值之和最…

Spring的注入小技巧(接口前置处理,后置处理等优化写法)

目录 1.定一个公共&#xff08;前置、后置&#xff09;接口 2.添加接口的实现类&#xff08;就是不同的处理&#xff09; 3.测试小栗子 4.执行结果 接口的前置处理或是后置处理&#xff0c;这样写代码更优雅&#xff0c;可读性高&#xff0c;当然更有水平更装逼。前置处理或…

【信号处理】基于变分自编码器(VAE)的图片典型增强方法实现

关于 深度学习中&#xff0c;经常面临图片数据量较小的问题&#xff0c;此时&#xff0c;对数据进行增强&#xff0c;显得比较重要。传统的图片增强方法包括剪切&#xff0c;增加噪声&#xff0c;改变对比度等等方法&#xff0c;但是&#xff0c;对于后端任务的性能提升有限。…

运算符规则

console.log(null undefined) null和undefined都是原始类型&#xff0c;然后把这两个转换为数字。是0NaN.看规则有一个NaN的话就得到NaN. console.log({} []); 把{}和[]转换为原始类型分别为和[Object Object]。然后特殊情况有字符串&#xff0c;那就拼接字符串返回[Object…

Redis数据库——群集(主从、哨兵)

目录 前言 一、主从复制 1.基本原理 2.作用 3.流程 4.搭建主动复制 4.1环境准备 4.2修改主服务器配置 4.3从服务器配置&#xff08;Slave1和Slave2&#xff09; 4.4查看主从复制信息 4.5验证主从复制 二、哨兵模式——Sentinel 1.定义 2.原理 3.作用 4.组成 5.…