跳转到主内容
极星编程网:以代码为星,赴技术山海!

c# 如何用 C# 实现一个滑动窗口计数器来做限流

滑动窗口计数器的核心逻辑是实时维护一个随请求滑动的时间窗口,每次请求时仅统计窗口内(如过去1秒)的有效请求时间戳,并动态清理过期数据;它依赖ConcurrentQueue线程安全地存储带时间戳的请求记录,通过while循环持续出队过期项后再判断队列长度是否超限,避免误判与性能陷阱。 滑动窗口计数器的核心逻辑是什么 滑动窗口计数器不是简单地把时间切分成固定桶(如每秒一个桶),而是让窗口随请求实时滑动,比如“过去 1 秒内最多 100 次请求”。关键在于:每次请求到来时,只统计那些时间戳 ≥
DateTime.UtcNow.AddSeconds(-1)
的记录,丢弃过期数据。它比固定窗口更平滑,但需要维护带时间戳的请求历史——不能只用一个整数累加。 用
ConcurrentQueue
实现线程安全的滑动窗口 这是最轻量、无外部依赖的做法,适合中低并发(QPS ≤ 数千)。
ConcurrentQueue
天然线程安全,避免锁开销;每次请求入队,再循环出队过期时间戳,最后检查队列长度是否超限。 必须用
DateTime.UtcNow
(而非
DateTime.Now
),避免时区和夏令时干扰 出队操作不能只做一次——要 while 循环直到队首时间戳有效,否则会误判(例如多个请求在临界点涌入) 队列长度本身不直接代表当前请求数,必须先清理过期项再取
Count
(
ConcurrentQueue
的
Count
是 O(n),高并发下慎用;改用原子计数器更稳)
public class SlidingWindowCounter { private readonly ConcurrentQueue _timestamps = new(); private readonly int _windowSeconds; private readonly int _maxRequests; private readonly object _cleanupLock = new(); // 避免 Count 被频繁调用时反复遍历 public SlidingWindowCounter(int windowSeconds, int maxRequests) { _windowSeconds = windowSeconds; _maxRequests = maxRequests; } public bool TryAcquire() { var now = DateTime.UtcNow; var windowStart = now.AddSeconds(-_windowSeconds); // 清理过期时间戳:必须 while 循环,不能只 pop 一次 while (_timestamps.TryPeek(out var ts) && ts < windowStart) { _timestamps.TryDequeue(out _); } // 使用锁保护 Count 访问(或改用 Interlocked 原子计数器) lock (_cleanupLock) { if (_timestamps.Count >= _maxRequests) return false; _timestamps.Enqueue(now); } return true; } }
为什么不用
SortedSet
或 Redis
SortedSet
看似能快速范围查询,但它不支持按时间范围批量删除(
RemoveWhere
是 O(n) 且非线程安全),实际性能不如队列 + 首尾扫描;而 Redis 的
ZREMRANGEBYSCORE
+
ZCARD
虽标准,但引入网络延迟和序列化开销,在单机高吞吐场景下反而成瓶颈。纯内存方案只要控制好队列大小(比如加个最大容量限制防内存泄漏),对大多数 Web API 限流已足够可靠。 Trae win Trae Windows 官方版本现已全面支持 Win10 和 Win11 系统(64位)。国内开发者可直接访问 Trae 中文官网(trae.cn 或 trae.com.cn),一键下载官方正版 .exe 安装包,享受国内直连的高速稳定体验。安装过程极其简便,双击即可运行,并支持一键无缝导入 VS Code 或 Cursor 的原有主题、快捷键及插件配置。登录支持手机号、微信或稀土掘金账号。内置免费的豆包与 DeepSeek 等顶级大模型,让你开箱即用,轻松开启高效的 AI 智能编程之旅。 下载 没做最大容量限制?极端情况下(长时间无请求后突发洪峰),队列可能积压数万条时间戳,导致清理变慢甚至 GC 压力上升 没考虑系统时钟回拨?如果 NTP 同步导致
UtcNow
突然变小,会误删大量未过期记录——生产环境建议搭配单调时钟(如
Stopwatch.GetTimestamp()
换算) 这个类不是完全无锁:
lock
块只保护
Count
和
Enqueue
,但清理逻辑本身是无锁的;若追求极致性能,可改用
Interlocked
维护计数器,把时间戳存进
ConcurrentBag
并定期批量清理(需额外调度) 如何在 ASP.NET Core 中集成并验证效果 别直接在 Controller 里 new 实例——每个请求新建一个计数器就完全失效了。必须注册为 Singleton,并通过依赖注入使用。同时,限流结果要明确返回 HTTP 429,不能静默失败。 注册服务:
services.AddSingleton(sp => new SlidingWindowCounter(1, 100));
在中间件或 ActionFilter 中调用
TryAcquire()
,失败时写入
context.Response.StatusCode = 429
并设置
Retry-After: 1
头 验证时用
curl -X GET http://localhost:5000/api/test &
快速并发 200 次,观察响应头和 状态码 分布;注意不要用 Postman 自带的“发送多次”功能——它默认串行,测不出并发效果 真正难的是边界场景:窗口跨越秒级边界时的计数抖动、多实例部署时的共享状态缺失、以及和熔断/降级策略的协同。单机滑动窗口只是起点,不是终点。

相关文章