当前位置:

首页 > 编程开发 > Java中经典限流算法实例分析

Java中经典限流算法实例分析

限流是什么?维基百科的概念如下:Incomputernetworks,ratelimitingisusedtocontroltherateofrequestssentorreceivedbyanetworkinterfacecontroller.ItcanbeusedtopreventDoSattacksandlimitwebscraping简单翻译一下:在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。限流,也称流量控制。是指系统在面临高并发,或者大流量请求的

本文目录

    限流是什么?

    维基百科的概念如下:

    In computer networks, rate limiting is used to control the rate of requests sent or received by a network interface controller. It can be used to prevent DoS attacks and limit web scraping

    简单翻译一下:在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。

    限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。举个生活的例子:

    ★

    一些热门的旅游景区,一般会对每日的旅游参观人数有限制的。每天只会卖出固定数目的门票,比如5000张。假设在五一、国庆假期,你去晚了,可能当天的票就已经卖完了,就无法进去游玩了。即使你进去了,排队也能排到你怀疑人生。

    ”

    常见的限流算法

    固定窗口限流算法

    首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。

    • 当次数少于限流阀值,就允许访问,并且计数器+1

    • 当次数大于限流阀值,就拒绝访问。

    • 当前的时间窗口过去之后,计数器清零。

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

    Java中经典限流算法实例分析

    伪代码如下:

        /**
         * 固定窗口时间算法
         * @return
         */
        boolean fixedWindowsTryAcquire() {
            long currentTime = System.currentTimeMillis();  //获取系统当前时间
            if (currentTime - lastRequestTime > windowUnit) {  //检查是否在时间窗口内
                counter = 0;  // 计数器清0
                lastRequestTime = currentTime;  //开启新的时间窗口
            }
            if (counter < threshold) {  // 小于阀值
                counter++;  //计数器加1
                return true;
            }
    
            return false;
        }

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

    Java中经典限流算法实例分析

    滑动窗口限流算法

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

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

    Java中经典限流算法实例分析

    假设单位时间还是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: 当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

    滑动窗口算法伪代码实现如下:

     /**
         * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
         */
        private int SUB_CYCLE = 10;
    
        /**
         * 每分钟限流请求数
         */
        private int thresholdPerMin = 100;
    
        /**
         * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
         */
        private final TreeMap counters = new TreeMap<>();
    
       /**
         * 滑动窗口时间算法实现
         */
        boolean slidingWindowsTryAcquire() {
            long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
            int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数
    
            //超过阀值限流
            if (currentWindowNum >= thresholdPerMin) {
                return false;
            }
    
            //计数器+1
            counters.get(currentWindowTime)++;
            return true;
        }
    
       /**
        * 统计当前窗口的请求数
        */
        private int countCurrentWindow(long currentWindowTime) {
            //计算窗口开始位置
            long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
            int count = 0;
    
            //遍历存储的计数器
            Iterator> iterator = counters.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry entry = iterator.next();
                // 删除无效过期的子窗口计数器
                if (entry.getKey() < startTime) {
                    iterator.remove();
                } else {
                    //累加当前窗口的所有计数器之和
                    count =count + entry.getValue();
                }
            }
            return count;
        }

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

    漏桶算法

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

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

    Java中经典限流算法实例分析

    • 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。

    • 桶的容量一般表示系统所能处理的请求数。

    • 如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)

    • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。

    漏桶算法伪代码实现如下:

     /**
         * 每秒处理数(出水率)
         */
        private long rate;
    
        /**
         *  当前剩余水量
         */
        private long currentWater;
    
        /**
         * 最后刷新时间
         */
        private long refreshTime;
    
        /**
         * 桶容量
         */
        private long capacity;
    
        /**
         * 漏桶算法
         * @return
         */
        boolean leakybucketLimitTryAcquire() {
            long currentTime = System.currentTimeMillis();  //获取系统当前时间
            long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
            long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
            refreshTime = currentTime; // 刷新时间
    
            // 当前剩余水量还是小于桶的容量,则请求放行
            if (currentWater < capacity) {
                currentWater++;
                return true;
            }
            
            // 当前剩余水量大于等于桶的容量,限流
            return false;
        }

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

    令牌桶算法

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

    令牌桶算法原理:

    • 有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。

    • 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。

    • 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;

    • 如果拿不到令牌,就直接拒绝这个请求。

    Java中经典限流算法实例分析

    漏桶算法伪代码实现如下:

        /**
         * 每秒处理数(放入令牌数量)
         */
        private long putTokenRate;
        
        /**
         * 最后刷新时间
         */
        private long refreshTime;
    
        /**
         * 令牌桶容量
         */
        private long capacity;
        
        /**
         * 当前桶内令牌数
         */
        private long currentToken = 0L;
    
        /**
         * 漏桶算法
         * @return
         */
        boolean tokenBucketTryAcquire() {
    
            long currentTime = System.currentTimeMillis();  //获取系统当前时间
            long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
            currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
            refreshTime = currentTime; // 刷新时间
            
            //桶里面还有令牌,请求正常处理
            if (currentToken > 0) {
                currentToken--; //令牌数量-1
                return true;
            }
            
            return false;
        }

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

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。