ASP.NET Core 的 Web Api 实现限流 中间件
一、背景与问题
在分布式系统中,API 接口的限流控制是保障系统稳定性和安全性的核心手段之一。随着系统访问量的激增,若不加限制地允许所有请求通过,可能导致以下问题:
- 服务器资源耗尽(CPU、内存、数据库连接等)
- 被恶意刷接口(DDoS 攻击)
- 系统性能下降(排队等待、超时等)
- 业务逻辑异常(如订单创建、支付等关键接口被滥用)
在 ASP.NET Core 中,通过自定义中间件实现限流是一种常见方案。本文将深入探讨限流中间件的实现原理,分析不同算法的适用场景,并提供完整的代码示例和性能优化建议。
二、基本原理
限流的核心思想是控制单位时间内的请求通过量。常见的限流算法包括:
- 固定窗口计数器(Fixed Window)
统计指定时间窗口内的请求数,超过阈值则拒绝。 - 滑动窗口(Sliding Window)
使用时间窗口的滑动机制,更精确地统计请求频率。 - 令牌桶(Token Bucket)
基于令牌生成的机制,支持突发流量和速率限制。 - 漏桶(Leaky Bucket)
基于固定速率的队列处理,保证请求的均匀性。
在 ASP.NET Core 中,限流中间件通常需要:
- 记录请求的时间戳
- 维护一个请求计数器
- 在请求到达时进行判断
- 根据策略决定是否放行或拒绝
三、环境准备
确保项目中已安装以下依赖:
dotnet add package Microsoft.AspNetCore.Http.Abstractions
dotnet add package Microsoft.AspNetCore.Mvc项目结构建议:
/Controllers
/Models
/Services
/Middleware
RateLimitMiddleware.cs
RateLimitOptions.cs
Startup.cs
Program.cs四、核心实现
1. 基于内存的固定窗口限流(Fixed Window)
// RateLimitOptions.cs
public class RateLimitOptions
{
public int MaxRequests { get; set; } = 100;
public int WindowSeconds { get; set; } = 60;
}// RateLimitMiddleware.cs
public class RateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly RateLimitOptions _options;
private readonly Dictionary<string, List<DateTime>> _requestTimes = new();
public RateLimitMiddleware(RequestDelegate next, IOptions<RateLimitOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task Invoke(HttpContext context)
{
var ipAddress = context.Connection.RemoteIpAddress.ToString();
// 获取当前窗口内请求时间
var windowStart = DateTime.UtcNow - TimeSpan.FromSeconds(_options.WindowSeconds);
var windowRequests = _requestTimes.ContainsKey(ipAddress)
? _requestTimes[ipAddress].Where(t => t >= windowStart).ToList()
: new List<DateTime>();
// 计算请求数
var requestCount = windowRequests.Count;
// 超过限制则拒绝
if (requestCount >= _options.MaxRequests)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsync("Too many requests");
return;
}
// 更新请求时间
_requestTimes[ipAddress] = windowRequests.Concat(new[] { DateTime.UtcNow }).ToList();
await _next(context);
}
}关键点说明:
- 使用字典记录每个客户端的请求时间戳
- 每次请求时计算窗口内请求数
- 通过字典的键值对实现内存存储
- 未使用并发锁,可能导致数据不一致(需在实际项目中处理)
2. 基于 Redis 的分布式限流(Sliding Window)
// RedisRateLimitMiddleware.cs
public class RedisRateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly RateLimitOptions _options;
private readonly IConnectionMultiplexer _redis;
public RedisRateLimitMiddleware(RequestDelegate next, IOptions<RateLimitOptions> options, IOptions<RedisOptions> redisOptions)
{
_next = next;
_options = options.Value;
_redis = ConnectionMultiplexer.Connect(redisOptions.Value.ConnectionString);
}
public async Task Invoke(HttpContext context)
{
var ipAddress = context.Connection.RemoteIpAddress.ToString();
var key = $"rate_limit:{ipAddress}";
var db = _redis.GetDatabase();
var currentTimestamp = DateTime.UtcNow.Ticks;
// 获取当前窗口内请求时间
var windowStart = currentTimestamp - _options.WindowSeconds * TimeSpan.TicksPerSecond;
var windowRequests = await db.HashGetAsync(key, "requests");
// 计算请求数
var requestCount = windowRequests.Length;
// 超过限制则拒绝
if (requestCount >= _options.MaxRequests)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsync("Too many requests");
return;
}
// 更新请求时间
await db.HashAddAsync(key, "requests", currentTimestamp);
await _next(context);
}
}关键点说明:
- 使用 Redis 的 Hash 结构存储请求时间戳
- 支持分布式部署,跨实例共享限流策略
- 需要配置 Redis 连接字符串(通过
appsettings.json)
3. 基于缓存的令牌桶算法(Token Bucket)
// TokenBucketRateLimitMiddleware.cs
public class TokenBucketRateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly RateLimitOptions _options;
private readonly Dictionary<string, (int tokens, DateTime lastRefill)> _buckets = new();
public TokenBucketRateLimitMiddleware(RequestDelegate next, IOptions<RateLimitOptions> options)
{
_next = next;
_options = options.Value;
}
public async Task Invoke(HttpContext context)
{
var ipAddress = context.Connection.RemoteIpAddress.ToString();
var bucket = _buckets.TryGetValue(ipAddress, out var bucket)
? bucket
: (tokens: _options.MaxRequests, lastRefill: DateTime.UtcNow);
var now = DateTime.UtcNow;
var timeSinceLastRefill = now - bucket.lastRefill;
var tokensToAdd = (int)(timeSinceLastRefill.TotalSeconds * _options.MaxRequests);
// 计算当前可用令牌
var currentTokens = Math.Min(bucket.tokens + tokensToAdd, _options.MaxRequests);
// 超过限制则拒绝
if (currentTokens < 1)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsync("Too many requests");
return;
}
// 消耗一个令牌
_buckets[ipAddress] = (currentTokens - 1, now);
await _next(context);
}
}关键点说明:
- 使用令牌桶算法,支持突发流量
- 令牌按固定速率补充
- 可调整最大容量和补充速率
五、完整案例
创建一个完整的限流服务,支持多种限流策略切换:
// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
// 注册限流中间件
app.UseRateLimiting(new RateLimitOptions
{
MaxRequests = 100,
WindowSeconds = 60
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}// RateLimitingExtensions.cs
public static class RateLimitingExtensions
{
public static IApplicationBuilder UseRateLimiting(
this IApplicationBuilder app,
RateLimitOptions options)
{
return app.UseMiddleware<RateLimitMiddleware>(options);
}
}// Controllers/RateLimitController.cs
[ApiController]
[Route("[controller]")]
public class RateLimitController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Rate limit is working");
}
}运行示例:
dotnet run访问 https://localhost:5001/RateLimit,前100次请求通过,第101次返回429。
六、源码解析
以固定窗口限流为例,关键代码流程如下:
- 记录请求时间
使用字典存储每个客户端的请求时间戳,避免频繁创建对象。 计算窗口内请求数
var windowStart = DateTime.UtcNow - TimeSpan.FromSeconds(_options.WindowSeconds); var windowRequests = _requestTimes.ContainsKey(ipAddress) ? _requestTimes[ipAddress].Where(t => t >= windowStart).ToList() : new List<DateTime>();判断是否超限
if (windowRequests.Count >= _options.MaxRequests) { context.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.Response.WriteAsync("Too many requests"); return; }更新请求时间
_requestTimes[ipAddress] = windowRequests.Concat(new[] { DateTime.UtcNow }).ToList();
注意:此实现未处理并发问题,实际生产环境中需要使用锁或原子操作。
七、进阶使用
1. 支持多策略切换
public class RateLimitOptions
{
public bool UseRedis { get; set; } = false;
public string RedisConnectionString { get; set; } = "localhost:6379";
}在中间件中根据配置选择实现:
if (_options.UseRedis)
{
var redisOptions = ...;
_redis = ConnectionMultiplexer.Connect(redisOptions.RedisConnectionString);
}2. 动态调整限流策略
通过 IOptionsMonitor 实现配置热更新:
var optionsMonitor = Options.Create(_options);
optionsMonitor.OnChange((_, _) =>
{
// 重新初始化限流策略
});3. 支持基于用户的限流
var userId = context.User.FindFirst("sub")?.Value;
var key = $"rate_limit:{userId}";八、性能与工程实践
1. 性能优化
- 内存限流:适合单机部署,但无法跨实例共享
- Redis 分布式限流:支持跨服务实例,但增加网络开销
- 缓存优化:使用
MemoryCache或Redis缓存请求时间戳
2. 异常处理
- 网络中断时的重试机制
- Redis 连接失败时的降级策略
- 高并发下的锁竞争优化
3. 安全风险
- IP 欺骗:攻击者可伪造 IP 地址绕过限流
- 缓存投毒:恶意用户可向缓存中写入虚假数据
- 解决方案:结合请求签名、JWT 等安全机制
九、常见问题与踩坑
1. 窗口计算错误
错误代码:
var windowStart = DateTime.UtcNow - _options.WindowSeconds;问题:未指定时间单位,可能导致计算错误
解决:明确使用 TimeSpan:
var windowStart = DateTime.UtcNow - TimeSpan.FromSeconds(_options.WindowSeconds);2. 未处理并发
错误代码:
_requestTimes[ipAddress] = windowRequests.Concat(new[] { DateTime.UtcNow }).ToList();问题:多线程环境下可能导致数据不一致
解决:使用并发锁或原子操作:
lock (_lockObject)
{
_requestTimes[ipAddress] = ...;
}3. Redis 连接未关闭
错误代码:
var redis = ConnectionMultiplexer.Connect("localhost:6379");问题:未在服务停止时释放资源
解决:使用 IDisposable 管理连接:
using (var redis = ConnectionMultiplexer.Connect("localhost:6379"))
{
// ...
}十、最佳实践
- 优先选择 Redis 分布式限流:适合微服务架构
- 结合 JWT 限流:对认证用户进行精细化控制
- 设置合理的限流阈值:根据业务需求调整
MaxRequests和WindowSeconds - 监控限流状态:通过日志或监控系统记录限流事件
- 支持降级策略:在极端情况下允许部分请求通过
十一、总结
ASP.NET Core 的限流中间件是保障系统稳定性的关键组件。本文深入探讨了固定窗口、滑动窗口和令牌桶三种常见限流算法的实现原理,并提供了完整的代码示例和性能优化建议。在实际开发中,应根据具体业务场景选择合适的限流策略,同时注意处理并发、安全和性能等问题。限流不仅是技术问题,更是系统设计的重要考量,需要结合业务需求进行综合评估。