如何用 Java 来构建一个简单的速率限制器?

news2024/11/18 1:43:18

速率限制

现实世界中的用户是残暴的,并且没耐心,充满着各种不确定性。在高并发系统中,可能会出现服务器被虚假请求轰炸的情况,因此您可能希望控制这种情况。

一些实际使用情形可能如下所示:

  1. API配额管理-作为提供者,您可能希望根据用户的付款情况限制向服务器发出API请求的速率。这可以在客户端或服务端实现。
  2. 安全性-防止DDOS攻击。
  3. 成本控制--这对服务方甚至客户方来说都不是必需的。如果某个组件以非常高的速率发出一个事件,它可能有助于控制它,它可能有助于控制从客户端发送的遥测。

限速处理时的选项

根据我们处理的请求/事件类型,可能会发生以下情况:

  1. 我们可以放弃额外的请求
  2. 我们可以选择让请求等待,直到系统将它们降低到预定义的速率。

常用限速算法

  1. 令牌桶算法
  2. 漏桶算法

我们将不深入讨论这些算法的内部细节,因为这超出了本文的范围。

我们将以令牌桶算法为中心。其要求如下。

令牌桶算法基于以固定速率添加令牌的固定容量桶的类比。在允许API继续之前,将检查桶,以查看它当时是否包含至少一个令牌。如果令牌存在,则进行API调用。如果不是,则丢弃该消息/或使其等待。

需求

  1. 应该能够接受每秒所需的(TPS)事务或速率。
  2. 如果超过我们定义的比率,则应放弃交易。
  3. 应该在同时发生的情况下起作用。

高级功能(在后续文章中实现)

  1. 应该能够平滑突发的请求。例如,如果我们将TPS定义为5,并且所有五个请求都在同一时刻到达,那么它应该能够以固定的时间间隔将它们排成一行,即以200ms的时间间隔执行每个请求。它需要一个内部定时电路。
  2. 如果我们的TPS为5,并且在其中一个1秒的时段中,我们在下一秒只使用3个代币,那么我们应该能够提供5+2 = 7个代币作为奖励。但速率为每个令牌1/7(142.28ms)。奖金不应结转到下一个插槽。

让我们首先定义我们的 速率限制器

/**
 * Rate limiter helps in limiting the rate of execution of a piece of code. The rate is defined in terms of
 * TPS(Transactions per second). Rate of 5 would suggest, 5 transactions/second. Transaction could be a DB call, API call,
 * or a simple function call.
 * <p>
 * Every {@link RateLimiter} implementation should implement either {@link RateLimiter#throttle(Code)} or, {@link RateLimiter#enter()}.
 * They can also choose to implement all.
 * <p>
 * {@link Code} represents a piece of code that needs to be rate limited. It could be a function call, if the code to be rate limited
 * spreads across multiple functions, we need to use entry() and exit() contract.
 */
public interface RateLimiter {

/**
     * Rate limits the code passed inside as an argument.
     *
     * @param code representation of the piece of code that needs to be rate limited.
     * @return true if executed, false otherwise.
     */
    boolean throttle(Code code);
    /**
     * When the piece of code that needs to be rate limited cannot be represented as a contiguous
     * code, then entry() should be used before we start executing the code. This brings the code inside the rate
     * limiting boundaries.
     *
     * @return true if the code will execute and false if rate limited.
     * <p
     */
    boolean enter();
    /**
     * Interface to represent a contiguous piece of code that needs to be rate limited.
     */
    interface Code {
        /**
         * Calling this function should execute the code that is delegated to this interface.
         */
        void invoke();
    }
}
复制代码

我们的 RateLimit有两组API:一个是throttle(code),另一个是enter()。这两种方法都满足相同的功能,但采用以下两种方式:

  1. boolean throttle(代码)-如果我们有连续的代码,可以用来传递一个代码块。
  2. 布尔输入() - 通常可以在API、DB或任何我们想要节流的调用之前使用。如果执行此代码后面的代码,则将返回 ,以及 假的如果它是速率受限的话。您可以将这些请求排队或拒绝。

在生产环境中您永远不会看到节流(代码)实现,因为它不是最佳的。请在评论中告诉我原因。大多数速率限制器使用类似于enter()的API

核心功能

为了构建速率限制器的核心,我们需要确保在任意两秒之间不允许超过N个事务。我们将如何做到这一点?

考虑我们进行第一笔交易的时刻t0。 t0 .所以,
直到(t0 + 1)s,我们只允许进行N次交易。 (t0 + 1)s , we are allowed to make only N transactions.如何确保这一点?在下次交易时,我们将检查
当前时间≤(t0 + 1)。.如果没有,那么这意味着我们进入了不同的秒,并且我们被允许进行N次交易。 N transactions.让我们看一小段代码,它演示了:

long now = System.nanoTime();
if (now <= mNextSecondBoundary) { // If we are within the time limit of current second
    if (mCounter < N) { // If token available
        mLastExecutionNanos = now;
        mCounter++; // Allocate token
        invoke(code); // Invoke the code passed the throttle method.
    }
}
复制代码

那么,我们如何定义mNextSecondBoundary呢?这将在我们进行第一个事务时完成,如前所述,我们将在完成第一个事务的时刻增加一秒。

if (mLastExecutionNanos == 0L) {
    mCounter++; // Allocate the very first token here.
    mLastExecutionNanos = System.nanoTime();
    mNextSecondBoundary = mLastExecutionNanos + NANO_PER_SEC;  // (10^9)
}
复制代码

现在,如果我们执行代码并看到我们进入了不同的秒,我们应该怎么做?我们将通过重置上次执行时间、可用令牌数来增强前面的代码,并通过调用 节流阀()再一次。我们的方法已经知道如何处理新的秒。

@Override
public boolean throttle(Code code) {
    if (mTPS <= 0) {
        // We do not want anything to pass.
        return false;
    }

synchronized (mLock) {
        if (mLastExecutionNanos == 0L) {
            mCounter++;
            mLastExecutionNanos = System.nanoTime();
            mNextSecondBoundary = mLastExecutionNanos + NANO_PER_SEC;
            invoke(code);
            return true;
        } else {
            long now = System.nanoTime();
            if (now <= mNextSecondBoundary) {
                if (mCounter < mTPS) {
                    mLastExecutionNanos = now;
                    mCounter++;
                    invoke(code);
                    return true;
                } else {
                    return false;
                }
            } else {
                // Reset the counter as we in a different second now.
                mCounter = 0;
                mLastExecutionNanos = 0L;
                mNextSecondBoundary = 0L;
                return throttle(code);
            }
        }
    }
}
复制代码

在这个实现中,我们可以传递需要节流的代码块,但是这个代码有一个问题。这将工作,但它会表现不佳。不推荐,但为什么呢?请在评论中告诉我。

现在,可以使用相同的构建块和enter()构建第二个API。我们将使用相同的逻辑,但我们不会执行方法内部的代码块。相反,它将在调用enter()之后执行,就像我们执行状态管理一样。该方法的实现如下:

@Override
public boolean enter() {
    if (mTPS == 0L) {
        return false;
    }

synchronized (mBoundaryLock) {
        if (mLastExecutionNanos == 0L) {
            mLastExecutionNanos = System.nanoTime();
            mCounter++;
            mNextSecondBoundary = mLastExecutionNanos + NANO_PER_SEC;
            return true;
        } else {
            long now = System.nanoTime();
            if (now <= mNextSecondBoundary) {
                if (mCounter < mTPS) {
                    mLastExecutionNanos = now;
                    mCounter++;
                    return true;
                } else return false;
            } else {
                // Reset the counter as we in a different second now.
                mCounter = 0;
                mLastExecutionNanos = 0L;
                mNextSecondBoundary = 0L;
                return enter();
            }
        }
    }
}
复制代码

现在,我们简单的速率限制器已经可以使用了。您可以查看完整的代码 这里。

结果

我们将尝试创建一个可创建六个线程的驱动程序代码。每个线程尝试从0到100计数,延迟为50ms(可以设置为任何数字)。我们将按如下方式启动我们的限速器:

public static void main(String[] args) {
    RateLimiter limiter = new SimpleTokenBucketRateLimiter(1);
    Thread[] group = new Thread[6];
    Runnable r = () -> {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (limiter.enter()) {
                System.out.println("Values:- " + Thread.currentThread().getName() + ": " + i);
            }
        }
    };


for (int i = 0; i < 6; i++) {
        group[i] = new Thread(r);
        group[i].start();
    }
}
复制代码

我们的API不支持平滑事务,而是让事务等待下一个令牌被分配,而不是丢弃请求。在拒绝它们之后,它返回false,所以如果我们真的想的话,我们可以把它们排队。

if (limiter.enter()) {
                System.out.println("Values:- " + Thread.currentThread().getName() + ": " + i);
} else { // queue the work again }
复制代码

这是TPS设置为1时的输出。

当我们尝试将TPS设置为 2我们将看到以下输出:

真管用!

从Android的角度看

  1. 考虑这样一种情况:您正在编写代码以捕获用户签名。当他们拖动指针时,您会捕获数千个点。平滑签名可能不需要所有这些参数,因此您使用速率限制进行采样。
  2. 一些事件调用频率很高。你能控制的。
  3. 我们有MessageQueue的空闲侦听器。当我们在主线程中侦听它时,它被随意调用。有时候,它在一秒钟内被调用好几次。如果我们想构建一个心跳系统来告诉我们主线程何时空闲,我们可以使用它来接收每秒的事件。如果我们一秒钟内没有收到事件,我们可以假定主线程处于忙碌状态。
  4. 对于您的框架/库的API配额管理,您可以根据用户选择的付款计划情况API调用。

今天先到这里吧。 我们将在后续文章中构建一个更复杂的速率限制器。

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

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

相关文章

28.函数指针变量作为函数的参数,容易混淆的指针概念,特殊指针,main函数传参

1.函数指针变量作为函数的参数 #include<stdio.h> int add(int x, int y) {return x y; } int sub(int x, int y) {return x - y; } int mux(int x, int y) {return x * y; } int dive(int x, int y) {return x / y; } int process(int(*p)(int, int),int x,int y) {in…

Redis基础命令操作三之集合类型SET

SET集合 特点&#xff1a;集合中存储的元素是惟一的。 命令举例说明SADD sadd [key] [value1 value2 value3]key对应的集合中添加元素SMEMBERSsmembers [key]获取key对应的集合的所有元素SISMEMBERsismember [key] [value]判断value是否在key对应的集合中存在SCARDscard [key…

excel日期函数:EDATE与DATE到底谁更胜一筹

平时的工作中&#xff0c;经常会遇到计算职工转正日期、合同到期日、职工退休日期以及产品有效期截止日等等与日期有关系的问题。这些问题看似复杂&#xff0c;实际上只需要一个很简单的函数就能搞定&#xff0c;这个函数就是EDATE。今天分享EDATE函数的几个应用实例&#xff0…

【Linux】Linux 权限和权限管理

文章目录Linux权限的概念Linux权限管理文件访问者的分类&#xff08;人&#xff09;文件类型和访问权限&#xff08;事物属性)目录权限默认权限粘滞位关于权限的总结Linux权限的概念 权限是用来限制人的&#xff0c;权限人事物属性 权限存在的意义是便于系统安全管理的 Linux下…

Python处理第一类切比雪夫多项式

第一类切比雪夫多项式简介 Chebyshev多项式是一种非常重要的正交多项式&#xff0c;在逼近理论中有重要应用&#xff0c;第一类切比雪夫多项式的根可用于多项式插值&#xff0c;对弥补龙格现象有很大的帮助。其表达形式为 Tncos⁡(narccos⁡x)T_n\cos(n\arccos x) Tn​cos(nar…

vite+vue3+Ts搭建基础项目框架

随着前端技术的更新&#xff0c;程序员们的技术栈也要不断跟上&#xff0c;本来想躺平&#xff0c;不料却被推着走。 上个月开发团队新来一个项目需求&#xff0c;要求开发技术栈vue2更换成vue3&#xff0c;毫无准备的小编一脸懵&#xff0c;嗯&#xff1f;怎么说 换就换了&am…

读写分离有哪些坑?

在上一篇文章中,我和你介绍了一主多从的结构以及切换流程。今天我们就继续聊聊一主多从架构的应用场景:读写分离,以及怎么处理主备延迟导致的读写分离问题。 我们在上一篇文章中提到的一主多从的结构,其实就是读写分离的基本结构了。这里,我再把这张图贴过来,方便你理解…

【Linux】Linux环境下如何实现自动化编译——make/makefile入门

文章目录 前言 一、make/Makefile 的使用 1.示例 2.编写 Makefile 文件 2.1生成 2.2清理 二、make 如何知道生成的可执行文件是否最新 前言 在Linux 环境下编写好C语言代码之后&#xff0c;我们需要使用编译工具gcc 将其翻译为可执行文件。可是&#xff0c;如果对代码进…

Apache Spark 机器学习 特征抽取 4-1

特征数据集是用于在机器学习中进行训练&#xff0c;有关特征的算法的分类如下所示&#xff1a; 抽取&#xff08;Extraction&#xff09;&#xff0c;从原始数据集中提取出对应的特征集 转换&#xff08;Transformation&#xff09;&#xff0c;缩放特征、转换特征以及修改特征…

线程池(关于变量捕获、线程数、针对ThreadPoolExecutor的构造方法参数的解释、自实现线程池)

目录&#xff1a;一、前言二、关于变量捕获三、针对ThreadPoolExecutor的构造方法参数的解释四、自实现线程池一、前言相比较于进程&#xff0c;创建线程 / 销毁线程 的开销是相对较小的&#xff0c;但是太过频繁的创建线程 / 销毁线程&#xff0c;其开销也很大。这时候我们就需…

C 语言编译链接

前言 一个 C 程序究竟是怎么变成可执行程序的&#xff0c;这其间发生了什么&#xff1f;本文将带你简要了解 C 程序编译过程&#xff0c;文章为 《程序员的自我修养—链接、装载与库》的读书笔记&#xff0c;更为详细的过程可以阅读原书。 比如下面一个经典的 C 程序&#xf…

百度飞浆在pycharm中的使用(含官网安装和cuda)

uieGitHub 安装cuda 1 获取版本 我的是 CUDA Toolkit 11.7.1 (August 2022), Versioned Online Documentation 为了防止后期版本不对应&#xff0c;我这里小心谨慎安装了August对应的月份。 C:\Users\89735>nvidia-smi Mon Dec 19 21:31:28 2022 ------------------------…

一眼万年,这3款顶级神软,内存满了也绝不卸载

免费软件都不好用&#xff1f;不&#xff01;下面3款良心软件&#xff0c;颠覆你的认知&#xff0c;功能强大到离谱&#xff0c;值得收藏往后有需要直接使用。 1、桌面运维助手 这是一款堪称神器的国产电脑优化工具&#xff0c;集硬件管理、系统管理、辅助工具于一体&#xff0…

Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

Effective C条款39&#xff1a;明智而审慎地使用private继承&#xff08;Use private inheritance judiciously&#xff09;条款39&#xff1a;明智而审慎地使用private继承1、private 继承2、在private继承和复合之间做出正确选择3、使用private继承比组合更加合理的例子4、牢…

wsl安装CUDA

NVCC 昨天已经安装好了gpu版的pytorch&#xff0c;对于一般的代码应该就可以运行了。但有些代码中需要用到cuda算子&#xff0c;需要配置nvcc环境。对于这个我也没能搞太清楚&#xff0c;网上的说法不一&#xff0c;我使用conda安装pytorch时也安装了cudatoolkit&#xff0c;按…

c++11 标准模板(STL)(std::forward_list)(八)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

当面试官问:“你还有什么要问我”,怎样回答才最加分?

面试到最后&#xff0c;面试官常常会问求职者&#xff1a;“你还有什么要问我&#xff1f;”许多人面对这个问题&#xff0c;不知该怎样回答&#xff0c;怕回答不好影响自己的面试结果&#xff0c;那么怎么回答才最加分呢&#xff1f;有人说&#xff0c;可以问问这个职位应该具…

springboot整合gateway网关

2.3 搭建Gateway 本项目使用Spring Cloud Gateway作为网关&#xff0c;下边创建网关工程。 新建一个网关工程。 工程结构 添加依赖&#xff1a; XML org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discove…

TryHackMe-Blog

Blog 比利乔尔&#xff08;Billy Joel&#xff09;在他的家用电脑上写了一个博客&#xff0c;并开始工作。这将是非常棒的&#xff01; 枚举此框并找到隐藏在其上的 2 个标志&#xff01;比利的笔记本电脑上有一些奇怪的事情。你能四处走动并得到你需要的东西吗&#xff1f;还…

蓝队常用的攻击手段

目录 一&#xff0c; 漏洞利用 1.1 SQL 注入漏洞 1.2 跨站漏洞 1.3 文件上传或下载漏洞 1.4 命令执行漏洞 1.5 敏感信息泄露漏洞 在实战过程中&#xff0c;蓝队专家根据实战攻防演练的任务特点逐渐总结出一套成熟的做法:外网纵向突破重点寻找薄弱点&#xff0c;围绕薄弱点…