从零开始掌握限流技术:计数器、滑动窗口、漏桶与令牌桶详解

news2024/9/25 13:19:24

为什么需要限流呢?

🔹想象一下,你的服务器就像一个繁忙的餐馆,而你的应用就像是餐馆的服务员。餐馆里人山人海,每个人都在争先恐后地想要点餐。这时候,如果没有一个好的限流机制,会发生什么呢?

  1. 厨房厨师忙不过来:

    • 如果没有限流,服务员会不停地接单,导致厨房里的厨师忙得团团转,最后可能连基本的炒菜都做不好了。
    • 限流就像是给餐馆安装了一个“排队机”,确保顾客有序地排队等待点餐,这样厨师就能从容不迫地做好每一盘菜。
  2. 顾客体验变差:

    • 如果不限制人数,餐馆可能会变得拥挤不堪,顾客需要等很久才能得到服务,甚至可能会有人因为等不及而离开。
    • 限流就像是安排一个“领位员”,确保顾客不会等太久,每个人都能享受到愉快的用餐体验。
  3. 资源耗尽:

    • 如果不限制人数,餐馆可能会消耗掉所有的食材,导致后面来的顾客无餐可吃。
    • 限流就像是合理分配食材,确保每个人都能吃到美味的食物,而不是让少数人吃得太多,其他人饿肚子。
  4. 系统崩溃:

    • 如果不限制请求,服务器可能会因为负载过高而崩溃,就像餐馆因为人太多而导致椅子、桌子倒塌一样。
    • 限流就像是给餐馆加上一些结实的家具,确保即使有很多人也能保持稳定。

🔸总之,限流就像是给餐馆安装了一个智能的“门卫”系统,确保餐馆既能容纳足够的顾客,又能保证每个人都能获得良好的服务。而对于服务器来说,限流有助于保护资源,提升用户体验,避免系统过载,确保服务的稳定性和可靠性。

什么场景下需要限流?

随着互联网业务的发展,如秒杀活动双十一促销等场景,系统经常会面临高并发流量的挑战。在这种情况下,如果不加以限制,系统可能会因为流量过大而崩溃。为了避免这种情况发生,就需要采取限流措施来保护系统。

限流是指在一定时间内对请求量或并发数进行限制,以保护系统免受过大的负载压力。这样可以确保系统在高并发的情况下仍然能够稳定运行,同时也能够防止恶意攻击。常见的限流算法,有计数器算法滑动窗口计数算法漏桶算法令牌桶算法

在这里插入图片描述

计数器算法

⭐️简介

计数器算法通过在一个固定周期内累加访问次数来限制请求数量。在这个算法中,系统会在设定的时间窗口内记录所有请求的次数,并设置一个最大阈值。当在一个时间窗口内的请求次数达到设定的阈值时,系统会触发拒绝策略,拒绝所有后续请求。每当时间窗口结束,计数器会自动重置为零,开始新的计数周期。

例如,如果我们设置系统在1秒内最多允许100次请求,那么在计数器算法中,这1秒就被划分为一个时间窗口(因此计数器算法也称为固定窗口算法Fixed Window)。在每个时间窗口中,计数器记录当前的请求数量。一旦请求数超过100次,所有后续请求将在这个时间窗口内被拒绝,直到1秒结束,计数器重新开始记录。

在这里插入图片描述

计数器算法虽然简单高效,但存在一个关键问题:它难以处理非均匀分布的流量峰值。例如,在一个1秒的时间窗口内,如果在第1.9秒和第2.1秒分别出现了100次的瞬时高并发请求,虽然这两个时间点分别落在不同的1秒窗口内,但实际上在很短的时间内系统承受了200次请求,超出了设定的阈值。尽管其他时间段的流量是正常的,这种短时间内超过阈值的情况仍然可能导致系统出现问题。

在这里插入图片描述

🔥代码实现(Java)

这段代码定义了一个 SimpleCounterRateLimiter 类,它可以用来限制每秒内的请求数量。当每秒内的请求数量达到或超过设定的最大值时,新的请求将被拒绝。

import java.time.LocalTime;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class MyFixedWindowRateLimiterDemo {
    // 阈值
    private static final int QPS = 2;
    // 计数器
    private static final AtomicInteger counter = new AtomicInteger();

    // 初始化调度器,定期重置计数器
    private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    static {
        scheduler.scheduleAtFixedRate(() -> {
            counter.set(0);
        }, 0, 1, TimeUnit.SECONDS);  // 每1秒重置计数器
    }

    public static boolean tryAcquire() {
        return counter.incrementAndGet() <= QPS;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread.sleep(250);
            LocalTime now = LocalTime.now();
            if (!tryAcquire()) {
                System.out.println(now + " 请求被限流");
            } else {
                System.out.println(now + " 请求通过");
            }
        }
        scheduler.shutdown(); // 在程序结束时关闭调度器
    }
}

❄️利弊

计数器限流算法简单高效,易于实现且资源消耗低,但其主要缺点是在处理非均匀分布的突发流量时可能无法准确反映瞬时峰值,从而导致系统过载。

滑动窗口算法

⭐️简介

滑动窗口算法改进了固定窗口算法中的临界问题,通过将固定窗口进一步细分为多个小周期,并为每个小周期记录请求次数。随着窗口向前滑动,过期的小周期会被移除,而在判断是否限流时,需累加当前窗口内所有小周期的请求次数并与阈值进行比较。这种方法能更准确地捕捉瞬时流量峰值,避免系统过载。

在这里插入图片描述

为了应对固定窗口算法中的临界问题,我们采用滑动窗口算法,将1秒的时间窗口细分为5个200毫秒的小周期,并记录每个小周期内的请求次数。以之前的场景为例,在1.9秒和2.1秒的时间点分别发生了100次恶意并发请求。当滑动窗口到达第5个小周期时,该周期内的请求次数为100次,尚未达到阈值。然而,当窗口滑动到第6个小周期时,累计请求次数变为200次,这时超过了阈值,从而触发了访问限制。

🔥代码实现(Java)

public class MySlidingWindowRateLimiterDemo {

    /** 队列id和队列的映射关系,队列里面存储的是每一次通过时候的时间戳,这样可以使得程序里有多个限流队列 */
    private volatile static Map<String, List<Long>> MAP = new ConcurrentHashMap<>();
    
    //阈值
    private static int QPS = 2;
    //时间窗口总大小(毫秒)
    private static long WindowSize = 10 * 1000;


    /**
     * 滑动时间窗口限流算法
     * 在指定时间窗口,指定限制次数内,是否允许通过
     *
     * @param listId     队列id
     * @param count      限制次数
     * @param timeWindow 时间窗口大小
     * @return 是否允许通过
     */
    public static synchronized boolean tryAcquire(String listId, int count, long timeWindow) {
        // 获取当前时间
        long nowTime = System.currentTimeMillis();
        // 根据队列id,取出对应的限流队列,若没有则创建
        List<Long> list = MAP.computeIfAbsent(listId, k -> new LinkedList<>());
        // 如果队列还没满,则允许通过,并添加当前时间戳到队列开始位置
        if (list.size() < count) {
            list.add(0, nowTime);
            return true;
        }

        // 队列已满(达到限制次数),则获取队列中最早添加的时间戳
        Long farTime = list.get(count - 1);
        // 用当前时间戳 减去 最早添加的时间戳
        if (nowTime - farTime <= timeWindow) {
            // 若结果小于等于timeWindow,则说明在timeWindow内,通过的次数大于count
            // 不允许通过
            return false;
        } else {
            // 若结果大于timeWindow,则说明在timeWindow内,通过的次数小于等于count
            // 允许通过,并删除最早添加的时间戳,将当前时间添加到队列开始位置
            list.remove(count - 1);
            list.add(0, nowTime);
            return true;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Thread.sleep(1000);
            LocalTime now = LocalTime.now();
            if (!tryAcquire("ip", QPS, WindowSize)) {// 任意10秒内,只允许2次通过;自定义限流规则“ip”
                System.out.println(now + " 请求被限流");
            } else {
                System.out.println(now + " 请求通过");
            }
        }
    }
}

❄️利弊

滑动窗口限流算法能够更精确地控制瞬时流量峰值,有效避免因突发请求而导致的系统过载,但其实现复杂度较高且资源消耗相较于简单计数器算法更大。

漏桶算法

⭐️简介

漏桶算法(Leaky Bucket) 是一个形象的比喻,它描述了一个桶(即队列)接收来自生产端的水(即请求或流量),并且这个桶底部有一个孔,以恒定的速度漏水(即消费端不断地处理队列中的请求)。如果水流入桶的速度超过了漏水的速度(即生产端的请求速率超过了消费端的处理能力),桶中的水就会逐渐积累。一旦桶内的水量超过了桶的容量,多余的水就会溢出(即请求被拒绝),从而实现了网络流量的整形和限流功能。

在这个模型中,漏水的速度代表了限流的阈值,即单位时间内系统可以处理的最大请求量。例如,如果QPS设置为100,则表示系统每秒最多可以处理100个请求。如果生产端的请求速率超过了这个阈值,请求就会在队列中堆积,最终导致超出桶的容量而被拒绝。

在这里插入图片描述

🔥代码实现(Java)

这个例子使用了一个 AtomicLong 作为桶中的水量,并使用 ScheduledExecutorService 来模拟桶漏水的过程。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class LeakyBucketRateLimiter {
    private final AtomicLong bucket = new AtomicLong(0L);
    private final long capacity;
    private final long leakRate;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final Runnable leakAction = this::leakBucket;

    public LeakyBucketRateLimiter(long capacity, long leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
        scheduleLeak();
    }

    public synchronized boolean allowRequest() {
        long currentWaterLevel = bucket.get();

        // 如果桶满了,拒绝请求
        if (currentWaterLevel >= capacity) {
            return false;
        }

        // 否则,添加一个单位的水
        bucket.addAndGet(1);
        return true;
    }

    private void leakBucket() {
        long currentWaterLevel = bucket.getAndAdd(-leakRate);
        if (currentWaterLevel - leakRate < 0) {
            bucket.set(0);
        }
    }

    private void scheduleLeak() {
        scheduler.scheduleAtFixedRate(leakAction, 1, 1, TimeUnit.SECONDS);
    }

    public static void main(String[] args) {
        LeakyBucketRateLimiter rateLimiter = new LeakyBucketRateLimiter(200, 10); // 容量200,每秒漏10个单位

        // 模拟客户端请求
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 500; i++) {
            int requestId = i;
            executor.submit(() -> {
                if (rateLimiter.allowRequest()) {
                    System.out.println("Request " + requestId + " is allowed.");
                } else {
                    System.out.println("Request " + requestId + " is denied.");
                }
            });
        }

        executor.shutdown();
    }
}

❄️利弊

漏桶算法能够平滑输入流量并确保系统负载稳定,但其主要缺点在于无法区分突发流量持续高负载,可能导致正常突发流量被错误地限制。

令牌桶算法

⭐️简介

令牌桶算法是漏桶算法的一种改进版本,常用于网络流量整形限流。它同样使用一个桶,但桶中存放的是固定数量的令牌。算法以一定的速率(阈值)向桶中发放令牌。每当一个请求到来时,必须先从桶中获取一个令牌才能继续处理,处理完成后令牌被丢弃;如果没有可用令牌,则执行拒绝策略(如直接拒绝或排队等待)。此外,向桶中发放令牌的动作是持续不断的,如果桶满则令牌会被丢弃。

在这里插入图片描述

表面上看,令牌桶算法和漏桶算法是相反的一个"进水",一个是"漏水"。但与漏桶算法实际不同的是,令牌桶不仅能够在限制客户端请求流量速率的同时还能允许一定程度的突发流量。限流和允许瞬时流量其实并不矛盾,在大多数场景中,小批量的突发流量系统是完全可以接受的。

在令牌桶算法中,令牌是持续不断地生成并存入到一个“桶”中。当网络流量相对平缓时,这个桶可以积累额外的令牌作为储备。一旦出现突发的大流量(即流量尖峰),这些储备的令牌就可以立即用于处理这些额外的请求,确保它们能够被快速处理而不必等待。

只有当流量超过了预设的最大阈值时,也就是桶中的令牌被耗尽之后,新的请求才会因为拿不到令牌而被延迟处理或直接拒绝。这种方式有助于保护后端系统免受流量峰值的影响,保持其稳定运行。

🔥代码实现(Java)

这里提供一下代码实现,单机下推荐使用(或者仿写)Google Guava自带的限流工具类RateLimiter

先引入依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.0-jre</version>
</dependency>

设置发放令牌的速率,然后直接调用tryAcquire,大家感兴趣的可以看看其源码实现

public class MyTokenBucketRateLimiterDemo {
    public static void main(String[] args) throws InterruptedException {
        // 每1s发放5个令牌到桶里
        RateLimiter rateLimiter = RateLimiter.create(5);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(100L);
            if(rateLimiter.tryAcquire()) {
                System.out.println("get one token");
            }else {
                System.out.println("no token");
            }
        }
    }
}

❄️利弊

漏桶算法相比,令牌桶算法的实现更为复杂。它不仅需要维护令牌的生成和消耗过程,还需要有精确的时间控制来确保令牌的生成速率符合预期,并且还需要一定的内存空间来存储这些令牌 尽管如此,令牌桶算法提供了更灵活的流量控制机制,允许一定程度上的突发流量处理,而不仅仅是简单地丢弃超出限制的数据包。

小结

算法名称优点缺点主要应用场景
漏桶算法- 实现简单
- 适用于简单的流量控制
- 无法处理突发流量
- 可能导致数据丢失
- 网络流量控制
- 基础的限流策略
令牌桶算法- 支持突发流量
- 可以调整速率
- 实现较复杂
- 需要更多内存
- API调用频率限制
- 网络流量控制
- 高级限流策略
固定窗口算法- 实现简单
- 易于理解和维护
- 在窗口切换时可能会导致瞬时的流量突变- 简单的API调用频率限制
- 基础的限流策略
滑动窗口算法- 能够平滑流量突变
- 提供更精确的限流
- 实现复杂度较高
- 需要维护更多的状态信息
- 高精度的API调用频率限制
- 网络流量控制
- 复杂的限流策略

当然了,每种算法都有其特定的应用场景和优缺点,选择哪种算法取决于具体的需求和环境

例如,如果你的应用需要支持突发流量并且不能轻易丢失数据,那么令牌桶算法可能是一个更好的选择。如果你的应用只需要简单的流量控制并且不需要支持突发流量,那么漏桶算法可能就足够了。对于需要更精确控制的场景滑动窗口算法可能是最佳选择。

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

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

相关文章

【轻松拿捏】设计模式六大基本原则(一)单一职责原则(SRP - Single Responsibility Principle)

&#x1f388;边走、边悟&#x1f388;迟早会好 一. 概述 单一职责原则&#xff08;SRP - Single Responsibility Principle&#xff09;是面向对象设计中的一个基本原则。它的核心思想是&#xff1a;一个类只应有一个引起它变化的原因&#xff0c;也就是说&#xff0c;一个类…

使用Docker-compose一键部署Wordpress平台

一、Docker-compose概述&#xff1a; docker-compose&#xff1a;单机容器编排 Dockerfile&#xff1a;先配置好文件&#xff0c;然后build&#xff0c;镜像——>容器。 docker-compose&#xff1a;即可基于Dockerfile&#xff0c;也可以基于镜像&#xff0c;可以一键式拉…

扎心“我学了六个月 Python,怎么还是会找不到工作”

前言 &#x1f449; 小编已经为大家准备好了完整的代码和完整的Python学习资料&#xff0c;朋友们如果需要可以扫描下方CSDN官方认证二维码或者点击链接免费领取【保证100%免费】 在编程界&#xff0c;Python是一种神奇的存在。有人认为&#xff0c;只有用Python才能优雅写代码…

qt-16可扩展对话框--隐藏和展现

可扩展对话框 知识点extension.hextension.cppmain.cpp运行图初始化隐藏展现--点击--详细按钮 知识点 MainLayout->setSizeConstraint(QLayout::SetFixedSize);//固定窗口大小 extension.h #ifndef EXTENSION_H #define EXTENSION_H#include <QDialog>class Extens…

如何对 GitLab 中文版进行升级?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门面向中国程序员和企业提供企业级一体化 DevOps 平台&#xff0c;用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规&#xff0c;而且所有的操作都是在一个平台上进行&#xff0c;省事省心省钱。可以一键安装极狐GitL…

EVE-NG导入锐捷镜像

安装搭建好了EVE-NG之后&#xff0c;我们想要做虚拟实验还需要载入虚拟镜像&#xff0c;下面我来演示如何导入锐捷镜像 准备工作&#xff1a; 1.安装EVE-NG 2.FTP远程传输工具 正式开始&#xff1a; 第一步&#xff1a;下载镜像 前往锐捷官网下载锐捷设备镜像 锐捷镜像下…

以太网链路聚合(LACP模式)

1. 实验目的 掌握使用静态LACP模式配置链路聚合的方法掌握控制静态LACP模式下控制活动链路的方法掌握静态LACP的部分特性的配置 实验拓扑 实验拓扑如图11-12所示&#xff1a; 图11-12以太网聚合LACP模式 3. 实验步骤 PC机IP地址的配置 PC1的配置&#xff0c;在ipv4下选…

内存(动态开辟)———C语言

内存管理&#xff1a; 1.C语言运行时的内存分配 2.static关键字 1.修饰变量 局部变量&#xff1a; <1>在编译的过程中&#xff0c;会在数据区为该变量开辟空间&#xff0c;如果代码中未对其进行初始化&#xff0c;则系统默认初始化为0。 <2>用static修饰的局部变量…

a股市场的股票怎么做期权交易

在金融市场中&#xff0c;A股股票期权是一种重要的衍生金融工具&#xff0c;它赋予持有者在未来某一特定日期以预定价格买入或卖出股票的权利。了解期权交易流程对于投资者至关重要&#xff0c;本文将详细介绍a股市场的股票怎么做期权交易&#xff1f;本文来自&#xff1a;期权…

【软件测试】测试阶段、评审、瀑布式流程

软件测试 测试阶段评审瀑布式流程 测试阶段 冒烟测试&#xff08;Smoke Testing&#xff09;&#xff1a; 冒烟测试是一种快速的初步测试&#xff0c;通常在开发周期的早期进行&#xff0c;用以验证软件的基本功能是否按预期工作。它通常在软件构建完成后立即执行&#xff0c;以…

【windows系统】应用与功能下卸载程序提示Windows找不到文件解决办法

我们使用windows系统的时候&#xff0c;想要删除某个程序&#xff0c;可以通过windows系统设置中的添加或删除程序来操作&#xff0c;如下图所示 打开如图下所示应用和功能界面&#xff0c;可以对程序进行卸载 如图下所示&#xff0c;我们可以通过这种方式卸载掉电脑中的程序 但…

ClickHouse集群的安装

目录 1.clickhouse中文文档地址 2.centos安装部署 2.1采用tgz的方式安装 2.2修改配置文件 2.3修改数据目录 2.4创建角色和目录 3 集群安装 3.1配置文件修改 3.2启动zookeeper 3.3启动clickhouse-server 3.4任意节点连接clickhouse 3.5查看集群 3.6建库 3.7查看数…

【Linux 驱动】IMX6ULL pinctrl驱动

1. 概述 Linux 驱动讲究驱动分离与分层&#xff0c;pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物。pinctrl顾名思义就是引脚控制&#xff0c;用来配置比如引脚mux复用信息&#xff0c;引脚电器属性&#xff08;比如上/下拉、速度、驱动能力等&#xff09;信息。gpio顾…

学会区分大模型——大模型的分类,让你更清晰的认识大模型

乱花渐欲迷人眼&#xff0c;学会从根本上认识问题 现在市面上大模型如百花齐放&#xff0c;对很多人来说一堆大模型带来的不是简单方便&#xff0c;而是乱七八糟以及迷茫。 因为不知道不同的大模型之间有什么区别&#xff0c;也不知道自己需要什么样的大模型&#xff1b;就拿…

【C++题解】1375. 拦截导弹方案求解

问题&#xff1a;1375. 拦截导弹方案求解 类型&#xff1a;贪心 题目描述&#xff1a; 某国为了防御敌国的导弹袭击&#xff0c;发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷&#xff1a; 虽然它的第一发炮弹能够到达任意的高度&#xff0c;但是以后每一发炮弹都…

理解Flink数据流图

前言 在大数据处理的世界里&#xff0c;Apache Flink 已经成为处理实时数据流的一个强大工具。Flink 提供了一种高度灵活的方法来构建复杂的数据处理管道&#xff0c;其核心是所谓的 DataFlow 图模型。本文将带你深入了解 Flink DataFlow 图的基础知识&#xff0c;帮助你理解它…

Hadoop入门基础(一):深入探索Hadoop内部处理流程与核心三剑客

在大数据的世界里&#xff0c;处理海量数据的需求越来越多&#xff0c;而Hadoop作为开源的分布式计算框架&#xff0c;成为了这一领域的核心技术之一。 一、Hadoop简介 Hadoop是Apache Software Foundation开发的一个开源分布式计算框架&#xff0c;旨在使用简单的编程模型来…

别再问了!微信小程序的那些事儿,一文搞定

微信小程序是一种无需下载安装即可使用的应用&#xff0c;它嵌入在微信生态中&#xff0c;用户通过微信扫一扫或搜索即可快速访问。 无论是购物、订餐、预约服务&#xff0c;还是玩个小游戏、看篇文章&#xff0c;都不需要下载额外的APP&#xff0c;直接就能在微信里搞定。不会…

Leading SAFe领导大规模敏捷认证公开课

课程简介 SAFe – Scaled Agile Framework是目前全球最广泛使用的大规模敏捷框架&#xff0c;也是全球敏捷相关认证中增长最快、最受认可的规模化敏捷认证。全球已有超过120万名SAFe认证专业人士。据官方统计&#xff0c;获得SAFe认证的IT专业人士平均工资增长13,000美元&…

C++:模拟实现string

前言&#xff1a; 为了更好的理解string底层的原理&#xff0c;我们将模拟实现string类中常用的函数接口。为了与std里的string进行区分&#xff0c;所以用命名空间来封装一个自己的strin类。 string.h #pragma once #define _CRT_SECURE_NO_WARNINGS 1#include<iostream&…