.NetCore手写一个 API 限流组件

news/2024/4/15 13:23:35

首先如果APP 拥有游客模式,用户模式以及其他特殊权限。那就意味着需要 IP 限流、用户限流以及特殊权限的情况。

那我们直接实操一下,以 IP 限流作为参考案例,当然要以组件的形式编写,支持扩展。

首先我们创建一个抽象类接口,定义一些限流行为和属性,我们需要针对限流的最小的单位,比如 IP、账号、设备号或者其他。使其每一个流量进来都需要记录访问者信息并且检查是否被限流。

public interface IRateLimiting
{//限流唯一键string Key { get; }// 访问+1void Visit();//检查是否限流bool Check();
}

上面定义的这个对象,只是一些简约的处理限流的行为,在我们面对复杂多变的业务场景时,IRateLimiting 不一定能够满足我们,在面对持续变化的业务,我们最好不要直接在这个对象里进行更改,而是新增加一个新的对象。

比如 ICodeRateLimiting,或者 IAccountRateLimiting 这种根据授权码,账号来满足。

但是对象一旦多起来了,我们总是需要使用它们,也就是实例化,我们能不能使用一个创建者来管理他们,我们需要哪个对象,就像创建者要就行。

public interface IRateLimitingCreator{/// <summary>/// 创建限流对象/// </summary>/// <param name="context">请求信息</param>/// <param name="max">最大次数</param>IRateLimitingInfo Create(HttpContext context, int max);}

将 IRateLimitingInfo 对象通过 Create 创建,接收 HttpContext 和 max 参数,分别是用户请求过来的上下文,里面包含了用户的信息,和另外一个参数 max ,告诉我们此次限流的最大是多少啊。

其次,我们需要一个处理执行者,来执行 IRateLimiting 对象信息。

这个执行者需要针对每一个流程进来时候更新信息到存储并且告诉我们是否被限流了。

public interface IRateLimitingExecute
{/// <summary>/// 更新限流信息并返回是否需要限流/// </summary>bool UpdateAndCheck(IRateLimitingInfo info);
}

接下来我们就要来实现 IRateLimiting 这个接口需要做的内容了,为了保持足够的扩展性,我们使用 abstract 来声明抽象类,比如说我实现了一套 IRateLimiting 通用的逻辑,你想要在我的基础之上进行修改符合自己业务的逻辑,就可以基础我的 abstract 类来进行扩展。

以上 RateLimiting 对象实现了 IRateLimiting 抽象接口要做的内容。

它记录了上次的信息和本次的信息,并且检查是否限流。

public abstract class RateLimiting : IRateLimiting{/// <summary>/// 当前请求次数/// </summary>private int _current_times;/// <summary>/// 当前值/// </summary>private int _current_value;/// <summary>/// 上次检测结果/// </summary>private int _last_times;/// <summary>/// 上一次请求时间/// </summary>private DateTime _lasttime = DateTime.Now;/// <summary>/// 访问量上限/// </summary>private int _limit;/// <summary>/// 当前key/// </summary>private string _key;/// <summary>/// 当前key/// </summary>public string Key => _key;public RateLimitingInfo(string key, int limit = 1200){_limit = limit;_key = key;}/// <summary>/// 判断是否需要限流/// </summary>/// <returns>true:限流,false:不限流</returns>public bool Check(){if (_last_times <= _limit){return _current_times <= _limit;}return false;}/// <summary>/// 当前访问次数+1/// </summary>public void Visit(){int currentValue = GetCurrentValue();if (_current_value != currentValue){_last_times = _current_times;_current_value = currentValue;_current_times = 1;}else{Interlocked.Increment(ref _current_times);}}/// <summary>/// 获取当前时间范围值/// </summary>public abstract int GetCurrentValue();}

比如我突然有一个业务需求就是,我想要通过按分钟来限流,每分钟单个 IP 限流 100 次。  

那我就可以在抽象类 RateLimiting 基础之上进行实现。

public class MinuteRateLimiting : RateLimiting{public MinuteRateLimiting(string key, int limit = 100): base(key, limit){}/// <summary>/// 当前分钟/// </summary>public override int GetCurrentValue(){return DateTime.Now.Minute;}

MinuteRateLimiting 类对抽象类 RateLimiting 的GetCurrentValue 方法进行实现。

按照分钟的维度来限流,同时对 key 和 limit 赋值。

也可以按照小时或天数:HourRateLimiting和DayRateLimiting 来实现。

接下来我们使用IRateLimitingCreator来实例化我们实现的 MinuteRateLimiting 对象。

public class MinuteIpRateLimitingCreator : IRateLimitingCreator{public IRateLimitingInfo Create(HttpContext context, int max){if (max <= 0){max = int.MaxValue;}return new MinuteRateLimitingInfo(context.GetRemoteIp(), max);}}
public class MinuteIpPathRateLimitingCreator : IRateLimitingCreator{//创建对象public IRateLimitingInfo Create(HttpContext context, int max){if (max <= 0){max = int.MaxValue;}return new MinuteRateLimitingInfo($"{context.GetRemoteIp()}:{context.GetCurrentClientId()}:{context.Request.Path}", max);}}

以上给了两个实现方式。分别是按照 IP 进行限流,或者按照请求的IP+ 请求ID+请求接口的路径来限流。

以上功能的整体就是,每个 IP 对应客户端 ID 针对某个请求路径,没分钟最大允许 {max} 次,超过即为启动限流。

在面对每次进来的流量时,我们总是需要记录这些流量,一般可以采用数据库记录,Redis,MemoryCache,本地缓存等等。

但是流量比较庞大的话,库表记录一般不是最佳的方式,更优选则是采用缓存的机制,例如 Redis,MemoryCache 等。

这里为了保证程序的轻量级和演示方便,这里采用 ConcurrentDictionary 线程安全的字典存储,并且允许 2 GB 的存储量。

public class RateLimitingCache : IRateLimiting{private ConcurrentDictionary<string, IRateLimitingInfo> _cache = new ConcurrentDictionary<string, IRateLimitingInfo>();public bool UpdateAndCheck(IRateLimitingInfo info){if (_cache.TryGetValue(info.Key, out var temp)){temp.Visit();_cache.AddOrUpdate(info.Key, temp, (string k, IRateLimitingInfo old) => temp);return temp.Check();}_cache.AddOrUpdate(info.Key, info, (string k, IRateLimitingInfo old) => info);return true;}}

ConcurrentDictionary:所有这些操作都是原子操作,都是线程安全的。

唯一的例外是接受委托的方法,即AddOrUpdate和GetOrAdd。

对于对字典的修改和写入操作,ConcurrentDictionary 请使用精细锁定来确保线程安全。字典上的读取操作以无锁方式执行。

但是,这些方法的委托在锁外部调用,以避免在锁下执行未知代码时可能出现的问题。因此,这些委托执行的代码不受操作原子性的约束。

以上内容基本上实现了功能,当然 RateLimitingCache 完全可以按照自己业务方式进行替换方案。

接下来我们将这套限流功能封装为一个组件,并且以中间件的方式进行注入。

public static class RateLimitingHelper{/// <summary>/// 配置默认限流/// 默认使用按每分钟ip访问次数进行限制/// </summary>public static void AddAIpRateLimiting(this IServiceCollection services, IConfiguration config){services.AddDefaultRateLimiting(config);services.AddTransient<IRateLimitingCreator, MinuteIpRateLimitingCreator>();}private static void AddDefaultRateLimiting(this IServiceCollection services, IConfiguration config){services.Configure<RateLimitingOption>(config.GetSection("RateLimiting"));services.AddSingleton<IRateLimiting, RateLimitingCache>();}}

这里我们采用每分钟 ip 访问次数进行限制,进行封装。

另外,我们将 max 限流的次数的设置暴露出去进行配置。

public class RateLimitingOption{/// <summary>/// 对应间隔最大的访问次数/// </summary>public int Times { get; set; } = 500;}

我们将其功能以中间件的形式进行配置。

/// <summary>/// 限流中间件/// </summary>public class RateLimitingMiddleware{/// <summary>/// 请求管道/// </summary>private readonly RequestDelegate _next;/// <summary>/// 日志记录/// </summary>private ILogger<RateLimitingMiddleware> _logger;/// <summary>/// 创建key/// </summary>private IRateLimitingCreator _creator;/// <summary>/// 限流接口/// </summary>private IRateLimiting _ratelimiting;/// <summary>/// 限流配置/// </summary>private RateLimitingOption _option;/// <summary>/// 日志记录中间件,用于记录访问日志/// </summary>public RateLimitingMiddleware(ILogger<RateLimitingMiddleware> log, IOptions<RateLimitingOption> options, IRateLimitingCreator creator, IRateLimiting ratelimiting, RequestDelegate next){_logger = log;_next = next;_creator = creator;_ratelimiting = ratelimiting;_option = options.Value;}/// <summary>/// 记录访问日志/// 先执行方法,后对执行的结果以及请求信息通过IVisitLogger进行日志记录/// </summary>public async Task Invoke(HttpContext context){IRateLimitingInfo rateLimitingInfo = _creator.Create(context, _option.Times);if (!_ratelimiting.UpdateAndCheck(rateLimitingInfo)){_logger.LogDebug("触发限流:" + rateLimitingInfo.Key);context.Response.StatusCode = 429;}else{await _next(context);}}}

最后,我们在 service 中注入服务:

//配置限流
services.AddAIpRateLimiting(Configuration);

注入中间件:

app.UseMiddleware<RateLimitingMiddleware>(Array.Empty<object>());

添加 appsettings 配置:

  "RateLimiting": {"Times": 5000},

到这里,即已经完成手写一套限流组件,为了适应业务的变化,我们的组件也完全适变化进行扩展。

图片

测试允许之后,如果设定时间内,超过了限定次数则接口会返回相应的限制信息。

图片

到这里我们就实现了一个手写且易扩展的 API 限流组件。


http://www.ppmy.cn/news/1221959.html

相关文章

数据结构-插入排序实现

文章目录 1、描述2、代码实现3、结果4、复杂度 1、描述 待排序的数组分为已排序、未排序两部分; 初始状态时&#xff0c;仅有第一个元素为已排序序列&#xff0c;第一个以外的元素为未排序序列&#xff1b; 此后遍历未排序序列&#xff0c; 将元素逐一插入到已排序的序列中&am…

Google codelab WebGPU入门教程源码<6> - 使用计算着色器实现计算元胞自动机之生命游戏模拟过程(源码)

对应的教程文章: https://codelabs.developers.google.com/your-first-webgpu-app?hlzh-cn#7 对应的源码执行效果: 对应的教程源码: 此处源码和教程本身提供的部分代码可能存在一点差异。点击画面&#xff0c;切换效果。 class Color4 {r: number;g: number;b: number;a…

layui的layer.confirm获取按钮焦点

因为ayer.confirm的按钮并非采用button&#xff0c;而是a标签&#xff0c;所以获取按钮焦点获取不到&#xff0c;要采用别的方法&#xff0c;下面介绍在ie11中和ie8中不同的写法 在ie11中 layer.confirm(确定取消这个弹窗吗&#xff1f;,{btn: [确定, 取消],success:function…

快速弄懂C++中的深拷贝和浅拷贝

浅拷贝 浅拷贝就是单纯拷贝指向该对象的内存&#xff0c;所以在进行多次浅拷贝后只是相当于多了几个指向同一个对象的指针&#xff0c;而深拷贝相当于完全复制了一个对象副本。浅拷贝指的是复制对象的所有成员变量的值&#xff0c;不管这些值是指针、基本数据类型还是其他对象…

判断json是否为空

文章目录 一、判断json是否为空 一、判断json是否为空 // 示例json数据 String jsonStr “{“name”:“张三”,“age”:25,“gender”:“男”}”; // 将json数据转换为JSONObject对象 JSONObject json JSONObject.parseObject(jsonStr); // 判断JSONObject对象是否为空 if (…

C语言测试题:用冒泡法对输入的10个字符由小到大排序 ,要求数组做为函数参数。

编写一个函数&#xff1a; 用冒泡法对输入的10个字符由小到大排序 &#xff0c;要求数组做为函数参数。 冒泡排序是一种简单的排序算法&#xff0c;它会多次遍历要排序的数列&#xff0c; 每次遍历时&#xff0c;依次比较相邻的两个元素&#xff0c;如果它们的顺序不符合要求…

【LeetCode】2656. K个元素的最大和

2656. K个元素的最大和 难度&#xff1a;简单 题目 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你需要执行以下操作 恰好 k 次&#xff0c;最大化你的得分&#xff1a; 从 nums 中选择一个元素 m 。将选中的元素 m 从数组中删除。将新元素 m 1 添加到数组中。你…

11.16 知识总结(模型层更多内容)

一、 多表查询&#xff08;跨表查询&#xff09; <br class"Apple-interchange-newline"><div></div> 子查询&#xff1a;分步查询 链表查询&#xff1a;把多个有关系的表拼接成一个大表(虚拟表) inner join left join right join 1.1 基于双下划…

无标题栏的Qt子窗体在父窗体中停靠时,如何做到严丝合缝

目录 1. 问题的提出 2. 一般实现 3. 加强版 1. 问题的提出 由于业务的要求&#xff0c;需要从父窗体弹出一个子窗体&#xff0c;该子窗体无标题栏&#xff0c;且该子窗体要停靠到父窗体右下角。这个看似很容易的问题&#xff0c;细研起来其实不容易&#xff01; 2. 一般实现…

高性能面试八股文之编译流程程序调度

1. C的编译流程 C语言程序的编译过程通常包括预处理&#xff08;Preprocessing&#xff09;、编译&#xff08;Compilation&#xff09;、汇编&#xff08;Assembly&#xff09;、链接&#xff08;Linking&#xff09;四个主要阶段。下面是这些阶段的详细说明&#xff1a; 1.…

云ES使用集群限流插件(aliyun-qos)

aliyun-qos插件是阿里云Elasticsearch团队自研的插件,能够提高集群的稳定性。该插件能够实现集群级别的读写限流,在关键时刻对指定索引降级,将流量控制在合适范围内。例如当上游业务无法进行流量控制时,尤其对于读请求业务,可根据aliyun-qos插件设置的规则,按照业务的优先…

组合数学(下):概率、博弈

概率 有限概率 &#x1f449;饱和式救援 【题目】 空间限制&#xff1a; 65536K ● 题目描述 在《流浪地球》电影中&#xff0c;地球上大部分的行星发动机被摧毁。 人类再一次展开全球性救援&#xff0c;现在告诉你每只救援队的目标发动机的编号以及这只救援队在成功救援的概…

在rt-thread中使用iperf触发断言卡死

问题触发 最近在适配sdio device驱动&#xff0c;CP芯片与AP芯片对接&#xff08;RK3399&#xff09;&#xff0c;准备使用iperf测试下能否AP与CP能否正常通信。CP芯片跑的是rt-thread系统&#xff0c;在使用sdio_eth_dev_init命令初始化后&#xff0c;使用iperf -c 192.168.1…

【vue】Vue项目中如何在父组件中直接调用子组件的方法

方案一&#xff1a;通过ref直接调用子组件的方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 //父组件中 <template> <div> <Button click"handleClick">点击调用子组件…

SQLite3 数据库学习(文章链接汇总)

参考引用 SQLite 权威指南&#xff08;第二版&#xff09;SQLite3 入门 SQLite3 数据库学习&#xff08;一&#xff09;&#xff1a;数据库和 SQLite 基础 SQLite3 数据库学习&#xff08;二&#xff09;&#xff1a;SQLite 中的 SQL 语句详解 SQLite3 数据库学习&#xff08;三…

17. Series.dt.month-提取日期数据中的月份信息

【目录】 文章目录 17. Series.dt.month-提取日期数据中的月份信息1. 知识回顾-创建一个Series对象2. 知识回顾-pd.to_datetime()将数据转换为pandas中的日期时间格式3. 实例化类相关知识4. Series.dt.month是什么&#xff1f;5. 如何使用Series.dt.month&#xff1f;6. Series…

[PyTorch][chapter 63][强化学习-QLearning]

前言&#xff1a; 这里结合走迷宫的例子,重点学习一下QLearning迭代更新算法 0,1,2,3,4 是房间&#xff0c;之间绿色的是代表可以走过去。 5为出口 可以用下图表示 目录&#xff1a; 策略评估 策略改进 迭代算法 走迷宫实现Python 一 策略评估 强化学习最终是为了…

2311rust到27版本更新

1.23 从Rust1.0开始,有叫AsciiExt的特征来提供u8,char,[u8]和str上的ASCII相关功能.要使用它,需要如下编写代码: use std::ascii::AsciiExt; let ascii a; let non_ascii ; let int_ascii 97; assert!(ascii.is_ascii()); assert!(!non_ascii.is_ascii()); assert!(int_a…

【Qt之QSplashScreen】开场动画使用:进度条加载及设置鼠标指针不转圈

效果 开场动画效果如下&#xff1a; 开场动画 介绍 QSplashScreen小部件提供了一个启动屏幕&#xff0c;可以在应用程序启动期间显示。 启动屏幕是一个小部件&#xff0c;通常在应用程序启动时显示。启动屏幕通常用于启动时间较长的应用程序(例如需要花费时间建立连接的数据…

LangChain(2):基于自己的文档构建一个问答系统

“”" 欢迎来到LangChain实战课 https://time.geekbang.org/column/intro/100617601 作者 黄佳 “”" 此笔记来自于 黄佳 的极客时间 LangChain 实战课。如有侵权请联系删除。 课程链接 课程github pip install pypdf pip install docx2txt pip install qdrant-clie…
最新文章