ASP.NET Core 的 Web Api 实现限流 中间件

ASP.NET Core 的 Web Api 实现限流 中间件

一、背景与问题

在分布式系统中,API 接口的限流控制是保障系统稳定性和安全性的核心手段之一。随着系统访问量的激增,若不加限制地允许所有请求通过,可能导致以下问题:

  1. 服务器资源耗尽(CPU、内存、数据库连接等)
  2. 被恶意刷接口(DDoS 攻击)
  3. 系统性能下降(排队等待、超时等)
  4. 业务逻辑异常(如订单创建、支付等关键接口被滥用)

在 ASP.NET Core 中,通过自定义中间件实现限流是一种常见方案。本文将深入探讨限流中间件的实现原理,分析不同算法的适用场景,并提供完整的代码示例和性能优化建议。


二、基本原理

限流的核心思想是控制单位时间内的请求通过量。常见的限流算法包括:

  1. 固定窗口计数器(Fixed Window)
    统计指定时间窗口内的请求数,超过阈值则拒绝。
  2. 滑动窗口(Sliding Window)
    使用时间窗口的滑动机制,更精确地统计请求频率。
  3. 令牌桶(Token Bucket)
    基于令牌生成的机制,支持突发流量和速率限制。
  4. 漏桶(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。


六、源码解析

以固定窗口限流为例,关键代码流程如下:

  1. 记录请求时间
    使用字典存储每个客户端的请求时间戳,避免频繁创建对象。
  2. 计算窗口内请求数

    var windowStart = DateTime.UtcNow - TimeSpan.FromSeconds(_options.WindowSeconds);
    var windowRequests = _requestTimes.ContainsKey(ipAddress)
        ? _requestTimes[ipAddress].Where(t => t >= windowStart).ToList()
        : new List<DateTime>();
  3. 判断是否超限

    if (windowRequests.Count >= _options.MaxRequests)
    {
        context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.Response.WriteAsync("Too many requests");
        return;
    }
  4. 更新请求时间

    _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"))
{
    // ...
}

十、最佳实践

  1. 优先选择 Redis 分布式限流:适合微服务架构
  2. 结合 JWT 限流:对认证用户进行精细化控制
  3. 设置合理的限流阈值:根据业务需求调整 MaxRequests 和 WindowSeconds
  4. 监控限流状态:通过日志或监控系统记录限流事件
  5. 支持降级策略:在极端情况下允许部分请求通过

十一、总结

ASP.NET Core 的限流中间件是保障系统稳定性的关键组件。本文深入探讨了固定窗口、滑动窗口和令牌桶三种常见限流算法的实现原理,并提供了完整的代码示例和性能优化建议。在实际开发中,应根据具体业务场景选择合适的限流策略,同时注意处理并发、安全和性能等问题。限流不仅是技术问题,更是系统设计的重要考量,需要结合业务需求进行综合评估。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日