.NetCore手写一个 API 限流组件

news/2025/3/21 1:39:34/

首先如果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 基于双下划…