基于 ASP.NET Core 的限流组件,提供令牌桶与漏桶两种算法,支持按路径前缀对请求进行限流。
- 支持令牌桶 / 漏桶两种限流算法
- 通过中间件对请求按路径前缀限流,命中即限流,不命中直接放行
- 核心算法基于"时间戳懒计算"实现:无后台线程、无锁、无队列,性能高
- 支持三种注册方式:单条规则、规则列表、配置文件
- 时钟回跳自愈:VM 在线迁移、容器暂停恢复等导致计时器后退时,自动恢复限流能力,不会长时间拒绝全部请求
RateLimiterOptions 各字段含义:
| 字段 | 说明 |
|---|---|
Path |
限流的路径前缀,如 /test(匹配规则见下文) |
LimiterType |
限流算法:1 = 令牌桶 TokenBucket,2 = 漏桶 LeakageBucket |
MaxQPS |
每秒放行速率,值越大放行越快 |
LimitSize |
桶容量,即最多允许的突发请求数。仅对令牌桶生效,漏桶不使用该值 |
若
MaxQPS <= 0会被钳制为1;若LimitSize <= 0会被钳制为50。同时存在上界,超出会抛
ArgumentOutOfRangeException(避免配置错误导致限流静默失效):
MaxQPS不得超过Stopwatch.Frequency——再高的速率无法被计时器分辨;LimitSize不得超过3600 × MaxQPS——即突发窗口上限为 1 小时。
- 非阻塞拒绝模式:超限时不排队、而是立即拒绝该请求(默认返回
503,可自定义)。 - 被拒时响应会带上
Retry-After头(整数秒,至少 1 秒),客户端可据此退避重试。 - 两种算法的差异体现在是否允许突发:
- 令牌桶(允许突发):启动后立即允许
LimitSize次突发请求,空闲会重新攒满突发头寸,之后按MaxQPS持续放行。- 示例:
MaxQPS=1, LimitSize=1表示启动后第一个请求放行,下一秒内继续放行 1 个,其余被拒。
- 示例:
- 漏桶(严格匀速):不保留突发头寸,请求按恒定节拍均速放行——两次放行严格间隔
1/MaxQPS秒,空闲不攒突发、同一瞬间只放行一个,过载时无尖峰。
- 令牌桶(允许突发):启动后立即允许
- 二者均采用"时间戳懒计算":无后台线程、无锁、无队列,性能高。
- 两种算法的突发额度都严格封顶在
LimitSize:闲置再久也只能攒满一个桶,不会攒出无上限的突发。 - 若计时器发生回跳(VM 在线迁移、容器暂停恢复等),水位领先量超出合理范围时会自动拉回当前时间自愈,避免长时间拒绝全部请求。
中间件使用 StartsWithSegments 做前缀段匹配(大小写不敏感):
| 配置路径 | 命中的请求 | 不命中的请求 |
|---|---|---|
/test |
/test、/test/limiter、/Test/x |
/testxxx、/contest |
- 多条规则同时命中时,采用最长前缀优先:
/api/private会优先于/api生效,与注册顺序无关。 - 命中即停止匹配,只扣减该路径对应限流器的一次额度;被拒后不会回退到更宽松的规则。
- 所有路径都未命中时直接放行。
代码注册单个规则:
services.AddRateLimiter(new RateLimiterOptions
{
LimiterType = LimiterType.TokenBucket,
LimitSize = 1,
MaxQPS = 1,
Path = "/test"
});代码注册多个规则:
services.AddRateLimiter(new List<RateLimiterOptions>
{
new RateLimiterOptions { LimiterType = LimiterType.TokenBucket, LimitSize = 1, MaxQPS = 1, Path = "/test" },
new RateLimiterOptions { LimiterType = LimiterType.LeakageBucket, LimitSize = 5, MaxQPS = 3, Path = "/api" }
});配置文件注册:
{
"RateLimiterOptions": [
{
"LimiterType": 2,
"LimitSize": 1,
"MaxQPS": 1,
"Path": "/test"
}
]
}services.AddRateLimiter(_configuration.GetSection("RateLimiterOptions"));被限流时默认返回 503 Service Unavailable:
app.UseRateLimiter();也可自定义回调(如返回 JSON / 其他状态码):
app.UseRouting();
app.UseRateLimiter(async context =>
{
context.Response.StatusCode = 503;
await context.Response.WriteAsync("Service Are Limit!!!");
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/test/limiter", async context =>
await context.Response.WriteAsync("Are You Ok!"));
});var limit = RateLimiter.Create(LimiterType.TokenBucket, 3, 5);
// 消耗 1 个令牌(默认)
if (limit.Acquire())
{
Console.WriteLine("获取成功");
}
else
{
Console.WriteLine($"获取失败,建议 {limit.TimeUntilNextSlot().TotalSeconds:F1}s 后重试");
}
// 也可一次消耗多个令牌:额度不足时直接失败,不会超发
// permits 不得超过 LimitSize——索取超过整个桶的额度没有意义
limit.Acquire(3);
ILimiterService不持有任何非托管资源,无需Dispose。实现是线程安全的,通常按路径注册为单例共享。