.NetCore手写一个 API 限流组件

news/2024/5/20 0:28:41 标签: .netcore, 开发语言, c#, asp.net

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

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

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

public interface IRateLimiting
{
    //限流唯一键
    string Key { get; }
    // 访问+1
    void 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.niftyadmin.cn/n/5190608.html

相关文章

基于STM32的蓝牙低功耗(BLE)通信方案设计与实现

蓝牙低功耗&#xff08;Bluetooth Low Energy&#xff0c;简称BLE&#xff09;是一种能够在低功耗环境下实现无线通信的技术。在物联网应用中&#xff0c;BLE被广泛应用于传感器数据采集、健康监测设备、智能家居等领域。本文将基于STM32微控制器&#xff0c;设计并实现一个简单…

go语言学习-基本概念与流程控制

1、hello world // package: 声明这个文件是属于哪个包的 // 什么是包&#xff1a;可以理解为Go源码的集合&#xff0c;也是一种比较高级的代码复用方案。 // 我们可以把一些复用的代码或者是功能封装在一起&#xff0c;然后形成一个包&#xff0c;可以被另外一个包进行引用&a…

python趣味编程-5分钟实现一个F1 赛车公路游戏(含源码、步骤讲解)

Python 中的 F1 赛车公路游戏及其源代码 F1 Race Road Game是用Python编程语言开发的,它是一个桌面应用程序。 这款 Python 语言的 F1 赛道游戏可以免费下载开源代码,它是为想要学习 Python 的初学者创建的。 该项目系统使用了 Pygame 和 Random 函数。 Pygame 是一组跨平…

Day33力扣打卡

打卡记录 最大和查询&#xff08;排序单调栈上二分&#xff09; 链接 大佬的题解 class Solution:def maximumSumQueries(self, nums1: List[int], nums2: List[int], queries: List[List[int]]) -> List[int]:ans [-1] * len(queries)a sorted(((a, b) for a, b in zi…

C_11练习题

一、单项选择题(本大题共20小题&#xff0c;每小题2分&#xff0c;共40分。在每小题给出的四个备选项中&#xff0c;选出一个正确的答案,并将所选项前的字母填写在答题纸的相应位置上。) 以下叙述中正确的是&#xff08;&#xff09; A.C语言不是一种高级语言 B.C语言不用编译…

在.net 6版本以上的web api中添加像.net 5一样的Startup.cs

Program.cs中&#xff1a; 第一步&#xff1a;在var builder WebApplication.CreateBuilder(args);的后面添加上&#xff1a; var startup new Startup(builder.Configuration); startup.ConfigureServices(builder.Services); 第二步&#xff1a;在var app builder.Build()…

Stable Diffusion - StableDiffusion WebUI 软件升级与扩展兼容

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/134463035 目前&#xff0c;StableDiffusion WebUI 的版本是 1.6.0&#xff0c;同步更新 controlnet、tagcomplete、roop、easy-prompt-selector等…

FISCOBCOS入门(十)Truffle测试helloworld智能合约

本文带你从零开始搭建truffle以及编写迁移脚本和测试文件,并对测试文件的代码进行解释,让你更深入的理解truffle测试智能合约的原理,制作不易,望一键三连 在windos终端内安装truffle npm install -g truffle 安装truffle时可能出现网络报错,多试几次即可 truffle --vers…