有一位朋友,在开发了一款对外使用的一款 APP,在测试阶段使用时突然被领导要求增加限流的功能。
我立马给她推荐了一些开源的组件,她表示不想用别人的组件,想要自己开发一个组件支持扩展符合自己的业务需求,并告诉我能不能给她一点思路。
秉承着我跟她的交情,我立马放下手中的活,开始构思能不能给她直接实操一遍基础代码,剩下的交给她自己。
首先她的 APP 拥有游客模式,用户模式以及其他特殊权限。那就意味着需要 IP 限流、用户限流以及特殊权限的情况。
那我们直接实操一下,以 IP 限流作为参考案例,当然要以组件的形式编写,支持扩展。
首先我们创建一个抽象类接口,定义一些限流行为和属性,我们需要针对限流的最小的单位,比如 IP、账号、设备号或者其他。使其每一个流量进来都需要记录访问者信息并且检查是否被限流。
public interface IRateLimiting
{
//限流唯一键
string Key { get; }
// 访问+1
void Visit();
//检查是否限流
bool Check();
}
上面定义的这个对象,只是一些简约的处理限流的行为,在我们面对复杂多变的业务场景时,IRateLimiting 不一定能够满足我们,在面对持续变化的业务,我们最好不要直接在这个对象里进行更改,而是新增加一个新的对象。
比如 ICodeRateLimiting,或者 IAccountRateLimiting 这种根据授权码,账号来满足。
但是对象一旦多起来了,我们总是需要使用它们,也就是实例化,我们能不能使用一个创建者来管理他们,我们需要哪个对象,就像创建者要就行。
public interface IRateLimitingCreator
{
///
/// 创建限流对象
///
/// 请求信息
/// 最大次数
IRateLimitingInfo Create(HttpContext context, int max);
}
将 IRateLimitingInfo 对象通过 Create 创建,接收 HttpContext 和 max 参数,分别是用户请求过来的上下文,里面包含了用户的信息,和另外一个参数 max ,告诉我们此次限流的最大是多少啊。
其次,我们需要一个处理执行者,来执行 IRateLimiting 对象信息。
这个执行者需要针对每一个流程进来时候更新信息到存储并且告诉我们是否被限流了。
public interface IRateLimitingExecute
{
///
/// 更新限流信息并返回是否需要限流
///
bool UpdateAndCheck(IRateLimitingInfo info);
}
接下来我们就要来实现 IRateLimiting 这个接口需要做的内容了,为了保持足够的扩展性,我们使用 abstract 来声明抽象类,比如说我实现了一套 IRateLimiting 通用的逻辑,你想要在我的基础之上进行修改符合自己业务的逻辑,就可以基础我的 abstract 类来进行扩展。
以上 RateLimiting 对象实现了 IRateLimiting 抽象接口要做的内容。
它记录了上次的信息和本次的信息,并且检查是否限流。
public abstract class RateLimiting : IRateLimiting
{
///
/// 当前请求次数
///
private int _current_times;
///
/// 当前值
///
private int _current_value;
///
/// 上次检测结果
///
private int _last_times;
///
/// 上一次请求时间
///
private DateTime _lasttime = DateTime.Now;
///
/// 访问量上限
///
private int _limit;
///
/// 当前key
///
private string _key;
///
/// 当前key
///
public string Key => _key;
public RateLimitingInfo(string key, int limit = 1200)
{
_limit = limit;
_key = key;
}
///
/// 判断是否需要限流
///
/// true:限流,false:不限流
public bool Check()
{
if (_last_times 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
{
///
/// 配置默认限流
/// 默认使用按每分钟ip访问次数进行限制
///
public static void AddAIpRateLimiting(this IServiceCollection services, IConfiguration config)
{
services.AddDefaultRateLimiting(config);
services.AddTransient();
}
private static void AddDefaultRateLimiting(this IServiceCollection services, IConfiguration config)
{
services.Configure(config.GetSection("RateLimiting"));
services.AddSingleton();
}
}
这里我们采用每分钟 ip 访问次数进行限制,进行封装。
另外,我们将 max 限流的次数的设置暴露出去进行配置。
public class RateLimitingOption
{
///
/// 对应间隔最大的访问次数
///
public int Times { get; set; } = 500;
}
我们将其功能以中间件的形式进行配置。
///
/// 限流中间件
///
public class RateLimitingMiddleware
{
///
/// 请求管道
///
private readonly RequestDelegate _next;
///
/// 日志记录
///
private ILogger _logger;
///
/// 创建key
///
private IRateLimitingCreator _creator;
///
/// 限流接口
///
private IRateLimiting _ratelimiting;
///
/// 限流配置
///
private RateLimitingOption _option;
///
/// 日志记录中间件,用于记录访问日志
///
public RateLimitingMiddleware(ILogger log, IOptions options, IRateLimitingCreator creator, IRateLimiting ratelimiting, RequestDelegate next)
{
_logger = log;
_next = next;
_creator = creator;
_ratelimiting = ratelimiting;
_option = options.Value;
}
///
/// 记录访问日志
/// 先执行方法,后对执行的结果以及请求信息通过IVisitLogger进行日志记录
///
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(Array.Empty());
添加 appsettings 配置:
"RateLimiting": {
"Times": 5000
},
到这里,即已经完成手写一套限流组件,为了适应业务的变化,我们的组件也完全适变化进行扩展。
测试允许之后,如果设定时间内,超过了限定次数则接口会返回相应的限制信息。
到这里我们就实现了一个手写且易扩展的 API 限流组件。