4种经典的限流算法与集群限流

news2024/9/25 1:17:22

0、基础知识

1000毫秒内,允许2个请求,其他请求全部拒绝。

不拒绝就可能往db打请求,把db干爆~

interval = 1000 

rate = 2;

一、固定窗口限流

固定窗口限流算法(Fixed Window Rate Limiting Algorithm)是一种最简单的限流算法,其原理是在固定时间窗口(单位时间)内限制请求的数量。通俗点说主要通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。

/**
 * 固定窗口限流算法
 * 比如1000ms 允许通过10个请求
 * @author jeb_lin
 * 14:14 2023/11/19
 */
public class FixWindowRateLimiter {

    private long interval; // 窗口的时间间隔
    private long rate; // 限制的调用次数
    private long lastTimeStamp; // 上次请求来的时间戳
    private AtomicLong counter; // 计数器

    public FixWindowRateLimiter(long interval,long rate){
        this.interval = interval;
        this.rate = rate;
        this.lastTimeStamp = System.currentTimeMillis();
        this.counter = new AtomicLong(0);
    }

    public static void main(String[] args) {
        // 比如1000ms 允许通过10个请求
        FixWindowRateLimiter limiter = new FixWindowRateLimiter(1000,2);

        for (int i = 0; i < 4; i++) {
            if(limiter.allow()){
                System.out.println(i + " -> ok ");
            } else {
                System.out.println(i + " -> no");
            }
        }


    }

    private boolean allow() {
        long now = System.currentTimeMillis();
        if(now - lastTimeStamp > interval){
            counter.set(0L);
            lastTimeStamp = now;
        }

        if(counter.get() >= rate){
            return false;
        } else {
            counter.incrementAndGet();
            return true;
        }
    }
}

输出:

0 -> ok 
1 -> ok 
2 -> no
3 -> no

优点

  1. 简单易懂

缺陷

  1. 存在临界问题

本来就允许1秒内进来2个请求,这时候进来了4个

 /**
     * 测试不正常的情况
     */
    private static void testUnNormal() throws Exception{
        // 比如1000ms 允许通过10个请求
        FixWindowRateLimiter limiter = new FixWindowRateLimiter(1000,2);

        Thread.sleep(500);
        for (int i = 0; i < 4; i++) {
            if(limiter.allow()){
                System.out.println(i + " -> ok ");
            } else {
                System.out.println(i + " -> no");
            }
            Thread.sleep(250);
        }
    }

输出:

0 -> ok 
1 -> ok 
2 -> ok 
3 -> ok 

二、滑动窗口限流

滑动窗口限流算法是一种常用的限流算法,它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。比如上图的示例中,每 500ms 滑动一次窗口就可以避免这种临界问题,可以发现窗口滑动的间隔越短,时间窗口的临界突变问题发生的概率也就越小,不过只要有时间窗口的存在,还是有可能发生时间窗口的临界突变问题。

类似于微积分:假如我切成1000个窗口,每1ms一个计数器

/**
 * 滑动窗口限流算法
 * 比如1000ms 允许通过 2 个请求
 *
 * @author jeb_lin
 * 14:14 2023/11/19
 */
public class SlidingWindowRateLimiter {

    private long interval; // 窗口的时间间隔
    private long maxRequest; // 限制的调用次数
    private Map<Long, AtomicLong> millToCount = null; // 假如1秒切成1000个窗口,也就是1毫秒一个计数器
    private LinkedList<Long> timeStampList = null; // 出现请求的那个时间戳,需要记录下来,用于后期的删除
    private AtomicLong counter; // 计数器

    public SlidingWindowRateLimiter(long interval, long maxRequest) {
        this.interval = interval;
        this.maxRequest = maxRequest;
        this.millToCount = new HashMap<>();
        this.timeStampList = new LinkedList<>();
        this.counter = new AtomicLong(0);
    }

    public static void main(String[] args) throws Exception {
        testNormal();
    }


    /**
     * 测试正常的情况
     */
    private static void testNormal() {
        // 比如1000ms 允许通过10个请求
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(1000, 2);

        for (int i = 0; i < 10; i++) {
            if (limiter.allow()) {
                System.out.println(i + " -> ok ");
            } else {
                System.out.println(i + " -> no");
            }
        }
    }

    private boolean allow() {
        long now = System.currentTimeMillis();

        // 剔除掉过期的窗口,比如现在是1001ms,那么1ms的窗口就需要剔除掉
        while (!timeStampList.isEmpty() && now - interval > timeStampList.getFirst()) {
            long timeStamp = timeStampList.poll();
            for (int i = 0; i < millToCount.getOrDefault(timeStamp,new AtomicLong(0)).get(); i++) {
                counter.decrementAndGet();
            }
            millToCount.remove(timeStamp);
        }

        if (counter.get() >= maxRequest) {
            return false;
        } else {
            timeStampList.add(now); // 当前出现成功请求,那么串上list

            AtomicLong timeStampCounter = millToCount.getOrDefault(now, new AtomicLong(0L));
            timeStampCounter.incrementAndGet();
            millToCount.put(now, timeStampCounter);
            counter.incrementAndGet();
            return true;
        }
    }
}

优点

  1. 简单易懂
  2. 精度高(通过调整时间窗口的大小来实现不同的限流效果)

缺陷

  1. 依旧存在临界问题,不可能无限小
  2. 占用更多内存空间

三、漏桶算法(固定消费速率)

漏桶限流算法(Leaky Bucket Algorithm)拥有更平滑的流量控制能力。其中漏桶是一个形象的比喻,这里可以用生产者消费者模式进行说明,请求是一个生产者,每一个请求都如一滴水,请求到来后放到一个队列(漏桶)中,而桶底有一个孔,不断的漏出水滴,就如消费者不断的在消费队列中的内容,消费的速率(漏出的速度)等于限流阈值。即假如 QPS 为 2,则每 1s / 2= 500ms 消费一次。漏桶的桶有大小,就如队列的容量,当请求堆积超过指定容量时,会触发拒绝策略。

类似于Kafka的消费者,在不调整配置的情况下,消费速度是固定的,多余的请求会积压,如果超过你kafka配置的topic最大磁盘容量,那么会丢消息。(topic是一个目录,topic下N个partition目录,假如你每个partition配置了1G的容量,那么超过这个这个容量,就会删除partition下的segement文件 xxx.index,xxx.log)

/**
 * 漏桶算法
 * 比如 1秒只能消费2个请求
 *
 * @author jeb_lin
 * 15:55 2023/11/19
 */
public class LeakyBucketRateLimiter {

    private int consumerRate; // 消费速度
    private Long interval; // 时间间隔,比如1000ms
    private int bucketCapacity; // 桶的容量
    private AtomicLong water; // 桶里面水滴数量

    public LeakyBucketRateLimiter(int consumerRate, Long interval, int bucketCapacity) {
        this.consumerRate = consumerRate;
        this.interval = interval;
        this.bucketCapacity = bucketCapacity;
        this.water = new AtomicLong(0);
        scheduledTask();
    }

    // 周期任务,比如每1000ms消费2个请求
    private void scheduledTask() {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
        service.scheduleAtFixedRate((Runnable) () -> {
            for (int i = 0; i < consumerRate && water.get() > 0; i++) {
                this.water.decrementAndGet();
            }
            System.out.println("water -> " + this.water.get());
        }, 0, interval, TimeUnit.MILLISECONDS);
    }

    public static void main(String[] args) {
        // 1000毫秒消费2个请求
        LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(2, 1000L, 10);

        for (int i = 0; i < 10; i++) {
            if (limiter.allow()) {
                System.out.println(i + "-> ok");
            } else {
                System.out.println(i + "-> no");
            }
        }

    }

    private boolean allow() {
        if (bucketCapacity < water.get() + 1) {
            return false;
        } else {
            water.incrementAndGet();
            return true;
        }
    }
}

输出:

0 -> ok 
1 -> ok 
2 -> no
3 -> no
4 -> no
5 -> no
6 -> no
7 -> no
8 -> no
9 -> no

优点

  1. 可以控制请求的处理速度,避免过载或者过度闲置,避免瞬间请求过多导致系统崩溃或者雪崩。
  2. 可以通过调整桶的大小和漏出速率来满足不同的限流需求(这个基本靠手动)

缺陷

  1. 需要对请求进行缓存,会增加服务器的内存消耗。
  2. 但是面对突发流量的时候,漏桶算法还是循规蹈矩地按照固定的速率处理请求。

四、令牌桶算法

令牌桶算法是一种常用的限流算法,相对于漏桶算法,它可以更好地应对突发流量和解决内存消耗的问题。该算法维护一个固定容量的令牌桶,每秒钟会向令牌桶中放入一定数量的令牌。当有请求到来时,如果令牌桶中有足够的令牌,则请求被允许通过并从令牌桶中消耗一个令牌,否则请求被拒绝。

4.2 令牌桶限流代码实现


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 令牌桶限流算法
 * 每 1000ms 生成2个令牌
 *
 * @author jeb_lin
 * 14:14 2023/11/19
 */
public class TokenBucketRateLimiter {

    private long interval; // 窗口的时间间隔
    private long rate; // 速率
    private AtomicLong tokenCounter; // 令牌的数量
    private final long bucketCapacity; // 桶的大小

    public TokenBucketRateLimiter(long interval, long rate ,long bucketCapacity) {
        this.interval = interval;
        this.rate = rate;
        this.tokenCounter = new AtomicLong(0);
        this.bucketCapacity = bucketCapacity;
        scheduledProduceToken();
    }

    private void scheduledProduceToken() {
        ExecutorService executorService = Executors.newScheduledThreadPool(1);
        ((ScheduledExecutorService) executorService).scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                // 桶里面放不下了
                if(tokenCounter.get() + rate > bucketCapacity){
                    System.out.println("bucket is full");
                    return;
                }
                long token = tokenCounter.addAndGet(rate);
                System.out.println("token -> " + token);
            }
        }, 0, interval, TimeUnit.MILLISECONDS);
    }

    public static void main(String[] args) throws Exception {
        testNormal();
    }

    /**
     * 测试正常的情况
     */
    private static void testNormal() throws Exception{
        // 比如1000ms 允许通过10个请求
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1000, 2,10);
        // 模拟瞬时流量,5秒前都没请求,5秒后瞬时流量上来了
        Thread.sleep(5000);
        for (int i = 0; i < 12; i++) {
            if (limiter.allow()) {
                System.out.println(i + " -> ok ");
            } else {
                System.out.println(i + " -> no");
            }
        }
    }

    private boolean allow() {
        if(tokenCounter.get() > 0){
            tokenCounter.getAndDecrement();
            return true;
        }

        return false;
    }
}

输出:

token -> 2
token -> 4
token -> 6
token -> 8
token -> 10
bucket is full
0 -> ok 
1 -> ok 
2 -> ok 
3 -> ok 
4 -> ok 
5 -> ok 
6 -> ok 
7 -> ok 
8 -> ok 
9 -> ok 
10 -> no
11 -> no

优点

Guava的RateLimiter限流组件,就是基于令牌桶算法实现的。

  1. 精度高:令牌桶算法可以根据实际情况动态调整生成令牌的速率,可以实现较高精度的限流。
  2. 弹性好:令牌桶算法可以处理突发流量,可以在短时间内提供更多的处理能力,以处理突发流量。

缺陷

  1. 实现复杂:在短时间内有大量请求到来时,可能会导致令牌桶中的令牌被快速消耗完,从而限流。这种情况下,可以考虑使用漏桶算法。(需要削峰填谷,学习kafka就用漏桶算法)
  2. 时间精度要求高:令牌桶算法需要在固定的时间间隔内生成令牌,因此要求时间精度较高,如果系统时间不准确,可能会导致限流效果不理想。

五、集群限流

参考阿里的 cluster-flow-control | Sentinelcluster-flow-controlicon-default.png?t=N7T8https://sentinelguard.io/zh-cn/docs/cluster-flow-control.html

FQA

1、为什么要有集群限流?

假如我的商品A服务,有20台服务器,每台服务器设置了单机限流,比如说100QPS,那么集群的QPS应该是2000,但是由于流量分配不均导致有200QPS打到了同一台机器,此时就触发了限流。

2、为什么集群限流推荐独立部署的模式?
Sentinel 提供了独立部署模式和嵌入应用的模式,假如是使用嵌入模式,也就是我的商品A服务里面有一台服务器是需要提供总体Token统计工作的,那么很容易出现单机故障,也容易出现假死的现象。因为这台机器本身是有业务在身的,本身是有负载的。(相同的原理出现在ES的Node节点管理上,es的Node节点有Master节点和Data节点,Data节点负责document的index和query,Master节点专心干集群和index的管理工作,比如新增删除index,分工明确,假如Master的功能放在Data上,那么可能由于某一时间来了一条特殊的query命令需要进行复杂的计算或者返回大数据的话,或者gc等情况,那么这个Node节点可能无法对外提供服务,造成假死现象,那么这个Master就会重新选举,到时候可能出现脑裂的情况)

3、剩余令牌数量,在哪管理?

假如我的商品服务A的接口Interface-1 设置的集群限流是 1000QPS,那么当目前有500QPS的时候,剩余令牌数量A1tokenCounter可以理解为剩下500个,那么这个500维护在哪?
众所周知,Token Server的服务,不可能只有一台机器,分布式部署下为了系统的高可用,Token Server假如有10台机器,

a、那么是这10台 Token Server机器都维护了商品服务A的接口Interface-1的 A1tokenCounter 数据吗?

如果是,那么如何更新,数据如何同步到各台机器?

b、如果数据不是在10台Token Server维护的,而是在Redis中维护的,然后每次都去请求Redis,那么接下来的问题是,Redis也是有Master和N台Slaver ,你读的是Slaver机器,那么主从延迟情况下,你怎么保证QPS不会超出你设定的QPS?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何从Android恢复出厂设置后的手机恢复数据

如果您已使用出厂设置删除了Android设备上的所有数据&#xff0c;或者有一段时间未使用&#xff0c;则需要恢复出厂设置以从Android设备中检索数据。 奇客数据恢复安卓版是一个有用的工具&#xff0c;可以在重置后检索Android数据。 将Android设备恢复出厂设置 如果您需要将A…

简单聊一聊幂等和防重

大家好&#xff0c;我是G探险者。 每年的双十一&#xff0c;618&#xff0c;电商系统都会面临这超高的流量&#xff0c;如果一个订单被反复提交&#xff0c;那电商系统如何保证这个订单之后执行一次减库存&#xff0c;扣款的操作&#xff1f; 这里就引入两个概念&#xff0c;…

integrin + Receptor ; platelet ; Ig-CAM

platelet HPA ; integrin ; Cell adhesion in cancer: Beyond the migration of single cells

SQL零基础入门教程,贼拉详细!贼拉简单! 速通数据库期末考!(十)

SQL 函数 SQL 拥有很多可用于计数和计算的内建函数。 比如&#xff1a; AVG() - 返回平均值 COUNT() - 返回行数 MAX() - 返回最大值 MIN() - 返回最小值 SUM() - 返回总和 FIRST() - 返回第一个记录的值 LAST() - 返回最后一个记录的值 GROUP BY 学习SQL函数前&#xff0c…

用GPT 搭建一个占星术、解梦、塔罗牌占卜和命理学服务

今天来尝试我们的占星术、解梦、塔罗牌占卜和命理学服务&#xff0c;揭开宇宙的奥秘并获得自我认识 聊天 GPT API 集成的 HTML5 模板。我们的目标是提供易于使用且高度可定制的 API 代码&#xff0c;使您能够训练自己的人工智能解决方案并将其添加到提示中。 我们的产品是可定…

JSP命令标签 静态包含/动态包含

好 下面我们聊聊JSP中的指令标签 这边 我们来说两个 分别是 静态包含 和 动态包含 我们可以将重用性代码包含起来 更好的使用 比如 我们界面上中下 分别有三个导航栏 那么 如果你写三份 就会出现很多重复代码 而且 改起来 也很不方便 要一次改三份 口说无凭 我们来做一个小案…

【Linux】第十九站:进程替换

文章目录 一、单进程版---最简单的程序替换二、进程替换的原理三、多进程的程序替换1.多进程的程序替换实例2.那么程序在替换时候有没有创建子进程呢3.再谈原理4.一个现象5.我们的CPU如何得知程序的入口地址&#xff1f; 四、各个接口的介绍1.execl2.execlp3.execv4.execvp5.ex…

【机器学习13】生成对抗网络

1 GANs的基本思想和训练过程 生成器用于合成“假”样本&#xff0c; 判别器用于判断输入的样本是真实的还是合成的。 生成器从先验分布中采得随机信号&#xff0c;经过神经网络的变换&#xff0c; 得到模拟样本&#xff1b; 判别器既接收来自生成器的模拟样本&#xff0c; 也接…

基于一致性算法的微电网分布式控制MATLAB仿真模型

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 本模型主要是基于一致性理论的自适应虚拟阻抗、二次电压补偿以及二次频率补偿&#xff0c;实现功率均分&#xff0c;保证电压以及频率稳定性。 一致性算法 分布式一致性控制主要分为两类&#xff1a;协调同…

Linux管道的工作过程

常用的匿名管道&#xff08;Anonymous Pipes&#xff09;&#xff0c;也即将多个命令串起来的竖线。管道的创建&#xff0c;需要通过下面这个系统调用。 int pipe(int fd[2]) 我们创建了一个管道 pipe&#xff0c;返回了两个文件描述符&#xff0c;这表示管道的两端&#xff…

『Spring Boot Actuator Spring Boot Admin』 实现应用监控管理

前言 本文将会使用 Spring Boot Actuator 组件实现应用监视和管理&#xff0c;同时结合 Spring Boot Admin 对 Actuator 中的信息进行界面化展示&#xff0c;监控应用的健康状况&#xff0c;提供实时警报功能 Spring Boot Actuator 简介 官方文档&#xff1a;Production-rea…

RocketMQ(三):集成SpringBoot

RocketMQ系列文章 RocketMQ(一)&#xff1a;基本概念和环境搭建 RocketMQ(二)&#xff1a;原生API快速入门 RocketMQ(三)&#xff1a;集成SpringBoot 目录 一、搭建环境二、不同类型消息1、同步消息2、异步消息3、单向消息4、延迟消息5、顺序消息6、带tag消息7、带key消息 一…

【IPC】 共享内存

1、概述 共享内存允许两个或者多个进程共享给定的存储区域。 共享内存的特点 1、 共享内存是进程间共享数据的一种最快的方法。 一个进程向共享的内存区域写入了数据&#xff0c;共享这个内存区域的所有进程就可以立刻看到 其中的内容。 2、使用共享内存要注意的是多个进程…

如何定位el-tree中的树节点当父元素滚动时如何定位子元素

使用到的方法 Element 接口的 scrollIntoView() 方法会滚动元素的父容器&#xff0c;使被调用 scrollIntoView() 的元素对用户可见。 参数 alignToTop可选 一个布尔值&#xff1a; 如果为 true&#xff0c;元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 scrollIntoV…

C语言入门笔记—static、extern、define、指针、结构体

一、static static修饰局部变量的时候&#xff0c;局部变量出了作用域&#xff0c;不销毁。本质上&#xff0c;static修饰局部变量的时候&#xff0c;改变了变量的存储位置。详见下图&#xff0c;当a不被static修饰和被static修饰的时候。 C/C static关键字详解&#xff…

随机过程-张灏

文章目录 导论随机过程相关 学习视频来源&#xff1a;https://www.bilibili.com/video/BV18p4y1u7NP?p1&vd_source5e8340c2f2373cdec3870278aedd8ca4 将持续更新—— 第一次更新&#xff1a;2023-11-19 导论 教材&#xff1a;《随机过程及其应用》陆大絟.张颢 参考&…

CD36 ; + Lectin;

CD2 LIMP-2&#xff0c; LGP85 SR-BI&#xff0c; CD36&#xff1b; 清道夫受体蛋白CD36超家族的成员是 脂质代谢 和 先天免疫 的重要调节因子。它们识别正常和修饰的脂蛋白&#xff0c;以及与病原体相关的分子模式。 该家族由三个成员组成&#xff1a; SR-BI &am…

ZJU Beamer学习手册(二)

ZJU Beamer学习手册基于 Overleaf 的 ZJU Beamer模板 进行解读&#xff0c;本文则基于该模版进行进一步修改。 参考文献 首先在frame文件夹中增加reference.tex文件&#xff0c;文件内容如下。这段代码对参考文献的引用进行了预处理。 \usepackage[backendbiber]{biblatex} \…

异常语法详解

异常语法详解 一&#xff1a;异常的分类&#xff1a;二&#xff1a;异常的处理1:异常的抛出:throw2&#xff1a;异常的声明:throws3&#xff1a;try-catch捕获并处理异常 三&#xff1a;finally关键字四&#xff1a;自定义异常类&#xff1a; 一&#xff1a;异常的分类&#xf…

电路的基本原理

文章目录 一、算数逻辑单元(ALU)1、功能2、组成 二、电路基本知识1、逻辑运算2、复合逻辑 三、加法器实现1、一位加法器2、串行加法器3、并行加法器 一、算数逻辑单元(ALU) 1、功能 算术运算&#xff1a;加、减、乘、除等 逻辑运算&#xff1a;与、或、非、异或等 辅助功能&am…