首先如果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 限流组件。