常见的限流算法分析以及手写实现(计数器、漏斗、令牌桶)

news2024/11/16 17:54:09

常见的限流算法分析

限流在我们日常生活中经常见到,如火车站门口的栏杆一些景点的门票只出售一定的数量 等等。在我们的开发中也用到了这种思想。

为什么要限流

🏫在保证可用的情况下尽可能多增加进入的人数,其余的人在排队等待,或者返回友好提示,保证里面的进行系统的用户可以正常使用, 防止系统雪崩

限流算法

🌴🌴限流算法很多,常见的有三类,分别是 计数器算法漏桶算法令牌桶算法

(1)计数器:

          在一段时间间隔内,处理请求的最大数量固定,超过部分不做处理。

(2)漏桶:

          漏桶大小固定,处理速度固定,但请求进入速度不固定(在突发情况请求过多时,会丢弃过多的请求)。

(3)令牌桶:

          令牌桶的大小固定,令牌的产生速度固定,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情况);每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。

计数器限流

🍺在一段时间间隔内,处理请求的最大数量固定,超过部分不做处理。

举个🌰,比如我们规定对于A接口,我们1分钟的访问次数不能超过100次。

那么我们可以这么做:

🎈在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多,拒绝访问;

🍬如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,就是这么简单粗暴。

代码实现: 😎

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
​
//计数器 限流
public class CounterLimiter {
​
    //起始时间
    private static long startTime = System.currentTimeMillis();
​
    //时间间隔1000ms
    private static long interval = 1000;
​
    //每个时间间隔内,限制数量
    private static long limit = 3;
​
    //累加器
    private static AtomicLong accumulator = new AtomicLong();
​
    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public static boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();
        //判断是否在上一个时间间隔内
        if (nowTime < startTime + interval) {
            //如果还在上个时间间隔内
            long count = accumulator.incrementAndGet();
            if (count <= limit) {
                return true;
            } else {
                return false;
            }
        } else {
            //如果不在上一个时间间隔内
            synchronized (CounterLimiter.class) {
                //防止重复初始化
                if (nowTime > startTime + interval) {
                    startTime = nowTime;
                    accumulator.set(0);
                }
            }
            //再次进行判断
            long count = accumulator.incrementAndGet();
            if (count <= limit) {
                return true;
            } else {
                return false;
            }
        }
    }
​
​
    // 测试
    public static void main(String[] args) {
​
        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
​
                    for (int j = 0; j < turns; j++) {
​
                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }
​
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }
​
}
​
复制代码

计数器限流的不足: 🌴

这个算法虽然简单,但是存在临界问题,我们看下图:

👉🏻从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。

🍦我们刚才规定的是1分钟最多100个请求(规划的吞吐量),也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。

用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。🙇🏻‍♀️

漏桶限流

✨漏桶算法限流的基本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝。 大致的漏桶限流规则如下:

(1)进水口(对应客户端请求)以任意速率流入进入漏桶。

(2)漏桶的容量是固定的,出水(放行)速率也是固定的。

(3)漏桶容量是不变的,如果处理速度太慢,桶内水量会超出了桶的容量,则后面流入的水滴会溢出,表示请求拒绝。

⭐漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶容量(capacity)则丢弃,因为桶容量是不变的,保证了整体的速率。 以一定速率流出水,

削峰📍: 有大量流量进入时,会发生溢出,从而限流保护服务可用

缓冲📍: 不至于直接请求到服务器, 缓冲压力

代码实现: 😎

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
​
//漏斗限流
public class LeakBucketLimiter {
​
    //桶的大小
    private static long capacity = 10;
    //流出速率,每秒两个
    private static long rate = 2;
    //开始时间
    private static long startTime = System.currentTimeMillis();
    //桶中剩余的水
    private static AtomicLong water = new AtomicLong();
​
    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized static boolean tryAcquire() {
        //如果桶的余量问0,直接放行
        if (water.get() == 0) {
            startTime = System.currentTimeMillis();
            water.set(1);
            return true;
        }
        //计算从当前时间到开始时间流出的水,和现在桶中剩余的水
        //桶中剩余的水
        water.set(water.get() - (System.currentTimeMillis() - startTime) / 1000 * rate);
        //防止出现<0的情况
        water.set(Math.max(0, water.get()));
        //设置新的开始时间
        startTime += (System.currentTimeMillis() - startTime) / 1000 * 1000;
        //如果当前水小于容量,表示可以放行
        if (water.get() < capacity) {
            water.incrementAndGet();
            return true;
        } else {
            return false;
        }
    }
​
​
    // 测试
    public static void main(String[] args) {
​
        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
​
                    for (int j = 0; j < turns; j++) {
​
                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }
​
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }
​
}
复制代码

漏桶的不足:

漏桶的出水速度固定,也就是请求放行速度是固定的。 漏桶出口的速度固定,不能灵活的应对后端能力提升。比如,通过动态扩容,后端流量从1000QPS提升到1WQPS,漏桶没有办法。

令牌桶限流

🍬令牌桶算法中新请求到来时会从桶里拿走一个令牌,如果桶内没有令牌可拿,就拒绝服务。 当然,令牌的数量也是有上限的。令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。

令牌桶限流大致的规则如下:🙇🏻‍

(1)进水口按照某个速度,向桶中放入令牌。

(2)令牌的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。

(3)如果令牌的发放速度,慢于请求到来速度,桶内就无牌可领,请求就会被拒绝。

总之,令牌的发送速率可以设置,从而可以对突发的出口流量进行有效的应对。

代码实现: 😎

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
​
//令牌桶
public class TokenBucketLimiter {
    //桶的容量
    private static long capacity = 10;
    //放入令牌的速率,每秒2个
    private static long rate = 2;
    //上次放置令牌的时间
    private static long lastTime = System.currentTimeMillis();
    //桶中令牌的余量
    private static AtomicLong tokenNum = new AtomicLong();
​
    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized static boolean tryAcquire() {
        //更新桶中剩余令牌的数量
        long now = System.currentTimeMillis();
        tokenNum.addAndGet((now - lastTime) / 1000 * rate);
        tokenNum.set(Math.min(capacity, tokenNum.get()));
        //更新时间
        lastTime += (now - lastTime) / 1000 * 1000;
        //桶中还有令牌就放行
        if (tokenNum.get() > 0) {
            tokenNum.decrementAndGet();
            return true;
        } else {
            return false;
        }
    }
​
​
    //测试
    public static void main(String[] args) {
​
        //线程池,用于多线程模拟测试
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 被限制的次数
        AtomicInteger limited = new AtomicInteger(0);
        // 线程数
        final int threads = 2;
        // 每条线程的执行轮数
        final int turns = 20;
        // 同步器
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        long start = System.currentTimeMillis();
        for (int i = 0; i < threads; i++) {
            pool.submit(() ->
            {
                try {
​
                    for (int j = 0; j < turns; j++) {
​
                        boolean flag = tryAcquire();
                        if (!flag) {
                            // 被限制的次数累积
                            limited.getAndIncrement();
                        }
                        Thread.sleep(200);
                    }
​
                } catch (Exception e) {
                    e.printStackTrace();
                }
                //等待所有线程结束
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        float time = (System.currentTimeMillis() - start) / 1000F;
        //输出统计结果
        System.out.println("限制的次数为:" + limited.get() +
                ",通过的次数为:" + (threads * turns - limited.get()));
        System.out.println("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
        System.out.println("运行的时长为:" + time + "s");
    }
​
}
复制代码

令牌桶的好处:

令牌桶的好处之一就是可以方便地应对 突发出口流量(后端能力的提升)。

比如,可以改变令牌的发放速度,算法能按照新的发送速率调大令牌的发放数量,使得出口突发流量能被处理。

🚀🚀🚀

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

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

相关文章

[附源码]JAVA毕业设计楼宇管理系统(系统+LW)

[附源码]JAVA毕业设计楼宇管理系统&#xff08;系统LW&#xff09; 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a;…

Python笔记 · 魔法函数 / Magic Methods / Dunder Methods

在上篇文章《Python笔记 私有方法、私有属性 & 单下划线、双下划线》我们介绍过以前置双下划线开始&#xff0c;后置双下划线结束的方法名&#xff1a;__*__&#xff0c;这是系统定义的一批特殊函数&#xff0c;通常被称之为&#xff1a;魔法函数&#xff08;Magic Method…

5-2:Kafka入门

Kafka简介 原本的kafka只是一个处理消息队列的技术&#xff0c;但随着功能不断增加&#xff0c;不断综合&#xff0c;成为了一个分布式的流媒体平台 Kafka是一个分布式的流媒体平台。 应用&#xff1a;消息系统、日志收集、用户行为追踪、流式处理。 Kafka特点 高吞吐量、消息…

电源模块测试解决方案-电源测试系统方案-电源模块测试报告NSAT-8000

*测试仪器&#xff1a;可编程直流电源、可编程直流电子负载、数字示波器、功率计 *测试产品&#xff1a;电源模块。纳米软件电源ATE自动测试系统适用于大功率工业电源、AC/DC类DC电源供应器、适配器、充电器、LED电源等开关电源之综合性能测试。 *被测项目&#xff1a;有效值电…

目标检测之多尺度融合

多尺度 卷积神经网络通过逐层抽象的方式来提取目标的特征&#xff0c;其中一个重要的概念就是感受野。 高层网络的感受野比较大&#xff0c;语义信息表征能力强&#xff0c;但是特征图的分辨率低&#xff0c;几何信息的表征能力弱&#xff08;空间几何特征细节缺乏&#xff09…

深入React源码揭开渲染更新流程的面纱

转前端一年半了&#xff0c;平时接触最多的框架就是React。在熟悉了其用法之后&#xff0c;避免不了想深入了解其实现原理&#xff0c;网上相关源码分析的文章挺多的&#xff0c;但是总感觉不如自己阅读理解来得深刻。于是话了几个周末去了解了一下常用的流程。也是通过这篇文章…

深入了解Spring循环依赖本质

说明: 1. 本文基于Spring-Framework 5.1.x版本讲解 2. 建议读者对创建对象部分源码有一定了解 概述 这篇讲讲Spring循环依赖的问题&#xff0c;网上讲循环依赖的帖子太多太多了&#xff0c;相信很多人也多多少少了解一点&#xff0c;那我还是把这个问题自己梳理一遍&#xff…

kubernetes,service详解下

kubernetes&#xff0c;service详解下 HeadLiness类型的Service 在某些场景中&#xff0c;开发人员可能不想使用Service提供的负载均衡功能&#xff0c;而希望自己来控制负载均衡策略&#xff0c;针对这种情况&#xff0c;kubernetes提供了HeadLiness Service&#xff0c;这类…

内存分段与内存分页:逻辑地址、物理地址、线性地址、虚拟地址

这篇文章也是我自己的博客网站的里的文章&#xff0c;我觉得这篇文章还是我觉得知识含量比较高的文章&#xff0c;所以特地把它发出来看看。 这篇文章写于我在写自己的操作系统JackOS的时候系统梳理了一下CPU访问内存的各种方式&#xff0c;写完这篇文章之后&#xff0c;我对C…

Kafka高级特性解析之生产者

1、消息发送 1.1、数据生产流程解析 Producer创建时&#xff0c;会创建一个Sender线程并设置为守护线程。生产消息时&#xff0c;内部其实是异步流程&#xff1b;生产的消息先经过拦截器->序列化器->分区器&#xff0c;然后将消息缓存在缓冲区&#xff08;该缓冲区也是在…

Docker桌面版安装与使用(windows)

目录一、Docker概念二、下载安装三、docker镜像安装与操作四、制作自己的python镜像容器五、目录挂载六、多容器通信七、Docker-Compose管理多个容器运行八、发布和部署九、备份数据迁移一、Docker概念 1、Docker 是一个应用打包、分发、部署的工具2、镜像Image、容器Containe…

Windows OpenGL 图像绿幕抠图

目录 一.OpenGL 图像绿幕抠图 1.原始图片2.效果演示 二.OpenGL 图像绿幕抠图源码下载三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 特效 零基础 Open…

[问题解决方案](多人共同合并场景)git已merge到master分支代码且被同事代码覆盖如何回退

git已merge到master分支代码如何回退&#xff08;多人共同合并&#xff09;场景已经被同事代码覆盖的解决方案&#xff08;无需强制合并权限&#xff09;代码revert后又需要重新启用怎么办如果是未受保护分支代码的回退且只有你一人合并的代码 可以直接使用下面的命令即可如果只…

【Unity3D日常开发】Unity3D中实现不规则Button按钮的精准响应

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客QQ群&#xff1a;1040082875 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在使用Unity3D开发中&#xff0c;可…

全志V853平台Camera模块开发框架详解

Camera 本章节介绍V853平台 Camera 模块的开发。 V853支持并口CSI、MIPI&#xff0c;使用VIN camera驱动框架。 Camera通路框架 VIN支持灵活配置单/双路输入双ISP多通路输出的规格 引入media框架实现pipeline管理 将libisp移植到用户空间解决GPL问题 将统计buffer独立为v…

Web大学生网页作业成品——抗击疫情网站设计与实现(HTML+CSS)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Vue3 样式绑定

Vue3 样式绑定1.Vue.js class2.class 属性绑定3.Vue.js style(内联样式)4.组件上使用 class 属性1.Vue.js class class 与 style 是 HTML 元素的属性&#xff0c;用于设置元素的样式&#xff0c;我们可以用 v-bind 来设置样式属性。 v-bind 在处理 class 和 style 时&#xf…

微信小程序反编译

本文转载于&#xff1a;https://www.cnblogs.com/one-seven/p/15524457.html 微信小程序反编译 微信文件保存位置\WeChat Files\Applet\小程序id_APP_.wxapkg 现在小程序是处于编码状态 github上下载一个python版的解密工具 https://github.com/superdashu/pc_wxapkg_decr…

【免杀前置课——Windows编程】十四、异步IO——什么是异步IO、API定位问题、APC调用队列

异步IO异步IO异步I/0注意事项:定位问题总解决方案APC调用队列异步IO 当我们读取一个文件时&#xff0c;一般情况下&#xff0c;线程是阻塞的&#xff0c;也就是说&#xff0c;当前线程在等待文件读取操作结束,这种方式叫同步IO。 Windows 在系统底层为用户实现了另外一种高效的…

【软考】-- 操作系统(下)

操作系统&#xff08;下&#xff09;第五节 文件管理&#x1f355;一、文件管理的基本概念1️⃣文件2️⃣文件目录3️⃣目录结构:&#x1f354;二、文件路径&#x1f35f;三、文件命名规则&#x1f32d;四、文件的基本操作&#x1f37f;五、文件类型与扩展名&#x1f9c2;六、系…