限流 Rate Limiting
大约 9 分钟约 2728 字
限流 Rate Limiting
简介
ASP.NET Core 7.0 内置了限流中间件,提供四种内置限流算法,支持端点级、全局级、分区级多种粒度控制。限流能保护服务免受过载请求冲击,是 API 网关和公共服务的关键防护手段。
特点
基本用法
注册限流中间件
/// <summary>
/// 全局限流配置
/// </summary>
var builder = WebApplication.CreateBuilder(args);
// 添加限流服务
builder.Services.AddRateLimiter(options =>
{
// 全局拒绝响应
options.RejectionStatusCode = 429;
// 全局限流策略
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User?.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 10
});
});
// 限流触发时的自定义响应
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
Error = "请求过于频繁,请稍后重试",
RetryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retry)
? retry.TotalSeconds
: 60
}, ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
app.Run();四种限流算法
1. 固定窗口限流
/// <summary>
/// 固定窗口 — 在固定时间窗口内限制请求数
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("FixedWindow", opt =>
{
opt.PermitLimit = 100; // 每个窗口最多 100 个请求
opt.Window = TimeSpan.FromMinutes(1); // 1 分钟窗口
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 20; // 队列最多排 20 个
});
});
// 在端点上应用
app.MapGet("/api/data", () => "数据")
.RequireRateLimiting("FixedWindow");2. 滑动窗口限流
/// <summary>
/// 滑动窗口 — 窗口随时间滑动,更平滑的限流
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("SlidingWindow", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6; // 分 6 段(每段 10 秒)
opt.QueueLimit = 10;
});
});
// 按 IP 限流
app.MapGet("/api/search", () => "搜索结果")
.RequireRateLimiting("SlidingWindow");3. 令牌桶限流
/// <summary>
/// 令牌桶 — 允许突发流量,令牌匀速补充
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.AddTokenBucketLimiter("TokenBucket", opt =>
{
opt.TokenLimit = 100; // 桶容量 100 个令牌
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10); // 每 10 秒补充一次
opt.TokensPerPeriod = 20; // 每次补充 20 个令牌
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 10;
});
});
// 适用于允许短时突发的场景(如文件上传)
app.MapPost("/api/upload", () => "上传成功")
.RequireRateLimiting("TokenBucket");4. 并发限流
/// <summary>
/// 并发限流 — 限制同时处理的请求数(非时间窗口)
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.AddConcurrencyLimiter("Concurrency", opt =>
{
opt.PermitLimit = 10; // 最多 10 个并发请求
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 50; // 等待队列最多 50 个
});
});
// 适用于资源密集型操作
app.MapPost("/api/report/generate", () => "报表生成中...")
.RequireRateLimiting("Concurrency");分区策略
按用户/IP 限流
/// <summary>
/// 分区限流 — 不同用户/IP 不同配额
/// </summary>
builder.Services.AddRateLimiter(options =>
{
// 按 IP 地址限流
options.AddPartitionedRateLimiter("ByIp", httpContext =>
{
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: ip,
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 30,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 3
});
});
// VIP 用户更高配额
options.AddPartitionedRateLimiter("ByUser", httpContext =>
{
var userId = httpContext.User?.FindFirst("sub")?.Value ?? "anonymous";
var isVip = httpContext.User?.IsInRole("VIP") ?? false;
return RateLimitPartition.GetTokenBucketLimiter(
partitionKey: userId,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = isVip ? 500 : 100,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = isVip ? 50 : 10
});
});
});端点级限流
/// <summary>
/// 不同端点使用不同限流策略
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
// 公开查询 — 宽松
options.AddFixedWindowLimiter("PublicQuery", opt =>
{
opt.PermitLimit = 60;
opt.Window = TimeSpan.FromMinutes(1);
});
// 写操作 — 严格
options.AddFixedWindowLimiter("Write", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromMinutes(1);
});
// 登录 — 极严格(防暴力破解)
options.AddFixedWindowLimiter("Auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
});
});
// 应用到端点
app.MapGet("/api/products", () => "产品列表")
.RequireRateLimiting("PublicQuery");
app.MapPost("/api/orders", () => "创建订单")
.RequireRateLimiting("Write");
app.MapPost("/api/login", () => "登录")
.RequireRateLimiting("Auth");
// 禁用限流的端点
app.MapGet("/health", () => "OK")
.DisableRateLimiting();自定义限流策略
基于业务的限流
/// <summary>
/// 自定义限流策略 — 根据业务规则动态限流
/// </summary>
public class BusinessRateLimiterPolicy : IRateLimiterPolicy<string>
{
private readonly ILogger<BusinessRateLimiterPolicy> _logger;
public BusinessRateLimiterPolicy(ILogger<BusinessRateLimiterPolicy> logger)
{
_logger = logger;
}
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected
{
get => async (context, ct) =>
{
_logger.LogWarning("请求被限流:{Path}", context.HttpContext.Request.Path);
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
Code = 429,
Message = "当前操作过于频繁,请稍后重试"
}, ct);
};
}
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
{
// 根据请求路径和用户组合分区
var userId = httpContext.User?.FindFirst("sub")?.Value ?? "anonymous";
var path = httpContext.Request.Path.Value;
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: $"{userId}:{path}",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 20,
Window = TimeSpan.FromMinutes(1)
});
}
}
// 注册自定义策略
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy<string, BusinessRateLimiterPolicy>("BusinessPolicy");
});分布式限流
使用 Redis 实现
/// <summary>
/// 分布式限流 — 多实例共享限流计数
/// 内置限流器是单机内存级别,分布式环境需要 Redis
/// </summary>
public class RedisRateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly ConnectionMultiplexer _redis;
private readonly ILogger _logger;
public RedisRateLimitMiddleware(RequestDelegate next, ConnectionMultiplexer redis, ILogger<RedisRateLimitMiddleware> logger)
{
_next = next;
_redis = redis;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var key = $"ratelimit:{ip}";
var db = _redis.GetDatabase();
// 滑动窗口计数
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var windowStart = now - 60;
// 使用 Redis Sorted Set 实现滑动窗口
var transaction = db.CreateTransaction();
_ = transaction.SortedSetRemoveRangeByScoreAsync(key, 0, windowStart);
_ = transaction.SortedSetAddAsync(key, now.ToString(), now);
var countTask = transaction.SortedSetLengthAsync(key);
_ = transaction.KeyExpireAsync(key, TimeSpan.FromSeconds(120));
await transaction.ExecuteAsync();
var count = await countTask;
if (count > 100) // 每分钟 100 次
{
context.Response.StatusCode = 429;
await context.Response.WriteAsJsonAsync(new { Error = "请求过于频繁" });
return;
}
await _next(context);
}
}四种算法对比
| 算法 | 特点 | 适用场景 |
|---|---|---|
| 固定窗口 | 简单,有突发问题 | 一般 API 限流 |
| 滑动窗口 | 更平滑,无突发问题 | 精确限流 |
| 令牌桶 | 允许突发,匀速补充 | 文件上传、消息发送 |
| 并发 | 限制同时处理数 | 资源密集型操作 |
限流高级场景
全局限流与端点限流组合
/// <summary>
/// 全局限流 + 端点限流的组合策略
/// </summary>
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
// 全局限流 — 所有端点共享
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var userId = context.User?.FindFirst("sub")?.Value ?? "anonymous";
return RateLimitPartition.GetTokenBucketLimiter(
partitionKey: userId,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 200,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 20,
QueueLimit = 0
});
});
// 登录端点严格限流
options.AddFixedWindowLimiter("Auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0;
});
// 文件上传 — 令牌桶
options.AddTokenBucketLimiter("Upload", opt =>
{
opt.TokenLimit = 10;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(30);
opt.TokensPerPeriod = 3;
opt.QueueLimit = 5;
});
// 报表生成 — 并发限流
options.AddConcurrencyLimiter("Report", opt =>
{
opt.PermitLimit = 3;
opt.QueueLimit = 10;
});
// 自定义拒绝响应
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = 429;
context.HttpContext.Response.Headers.Append("Retry-After",
context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retry)
? retry.TotalSeconds.ToString()
: "60");
await context.HttpContext.Response.WriteAsJsonAsync(new
{
Type = "https://httpstatuses.com/429",
Title = "请求过于频繁",
Status = 429,
Detail = "您发送了过多请求,请稍后重试"
}, ct);
};
});
// 端点应用
app.MapPost("/api/login", () => "登录")
.RequireRateLimiting("Auth");
app.MapPost("/api/upload", () => "上传成功")
.RequireRateLimiting("Upload");
app.MapPost("/api/report/generate", () => "生成中")
.RequireRateLimiting("Report");
app.MapGet("/api/data", () => "数据")
.RequireRateLimiting("Default");限流中间件执行顺序
/// <summary>
/// 限流中间件的正确放置顺序
/// 注意:UseRateLimiter 应该放在路由之后、端点之前
/// </summary>
var app = builder.Build();
// 正确的中间件顺序
app.UseExceptionHandler();
app.UseHsts();
app.UseHttpsRedirection();
app.UseCors("Strict");
app.UseAuthentication();
app.UseAuthorization();
// 限流放在认证之后 — 这样可以根据用户身份区分限流策略
app.UseRateLimiter();
app.MapControllers();
app.Run();限流与 API 网关配合
# Nginx 作为 API 网关时的限流配置
# /etc/nginx/conf.d/rate-limit.conf
# 定义限流区域
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=upload_limit:10m rate=10r/m;
server {
listen 80;
# 通用 API 限流
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
limit_req_status 429;
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 登录严格限流
location /api/login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
# 上传接口限流
location /api/upload {
limit_req zone=upload_limit burst=5 nodelay;
client_max_body_size 50m;
proxy_pass http://backend;
}
# 自定义 429 错误页面
error_page 429 = @rate_limited;
location @rate_limited {
default_type application/json;
return 429 '{"error":"Too many requests","retry_after":60}';
}
}限流监控与告警
/// <summary>
/// 限流事件监控 — 记录被限流的请求
/// </summary>
public class RateLimitLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitLoggingMiddleware> _logger;
private readonly Counter _rateLimitedCounter;
public RateLimitLoggingMiddleware(
RequestDelegate next,
ILogger<RateLimitLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
// Prometheus 指标
_rateLimitedCounter = Metrics.CreateCounter(
"http_rate_limited_total",
"被限流的请求总数",
new CounterConfiguration
{
LabelNames = new[] { "path", "client_ip" }
});
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
if (context.Response.StatusCode == 429)
{
var path = context.Request.Path;
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
_logger.LogWarning("请求被限流: Path={Path}, IP={IP}, User={User}",
path, clientIp,
context.User?.FindFirst("sub")?.Value ?? "anonymous");
_rateLimitedCounter.Labels(path, clientIp).Inc();
}
}
}
// 注册限流监控中间件
app.UseMiddleware<RateLimitLoggingMiddleware>();
app.UseRateLimiter();优点
缺点
总结
限流是 API 服务的基本防护手段。ASP.NET Core 内置限流中间件覆盖了主流场景:固定窗口用于一般 API,令牌桶允许突发,并发限流保护资源。单机用内置中间件,分布式用 Redis 实现。登录等敏感接口务必严格限流防暴力破解。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《限流 Rate Limiting》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《限流 Rate Limiting》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《限流 Rate Limiting》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《限流 Rate Limiting》最大的收益和代价分别是什么?
