直接用 Microsoft.Extensions.RateLimiting 是因它已内置线程安全、分布式支持的令牌桶实现,手写易遗漏竞态处理、时间精度和突发流量逻辑;核心参数需结合接口响应时间设定,如 PermitLimit 对应最大并发数,TokensPerPeriod 与 ReplenishmentPeriod 共同决定平均吞吐,QueueLimit 应根据超时容忍度合理设置。

为什么直接用 Microsoft.Extensions.RateLimiting 而不是手写令牌桶
ASP.NET Core 7+ 内置的限流中间件已默认采用令牌桶(TokenBucketRateLimiter)实现,且做了线程安全、跨请求共享、支持分布式(配合 IDistributedCache)等关键优化。手写容易漏掉:Interlocked 竞态处理、滑动窗口时间精度、突发流量下的令牌预分配逻辑。除非你明确需要自定义填充策略(比如按 CPU 使用率动态调速),否则不建议从零实现。
TokenBucketRateLimiter 的核心配置参数怎么设才合理
关键参数不是“桶大小”和“填充速率”两个数字,而是它们与业务响应时间的耦合关系。例如:接口平均耗时 200ms,但允许最多 5 个并发请求瞬间打进来,那么 PermitLimit 至少为 5;若希望每秒最多放行 10 次,则 QueueProcessingOrder 设为 Fifo,QueueLimit 建议不超 5(避免排队过长导致超时),ReplenishmentPeriod 设为 TimeSpan.FromSeconds(1),TokensPerPeriod 设为 10。
-
PermitLimit:桶容量,决定最大并发请求数,不是 QPS —— 它限制的是“同时能抢到令牌的请求数” -
TokensPerPeriod和ReplenishmentPeriod共同决定平均吞吐,但不保证每秒精确放行——令牌是随时间匀速填充的,不是定时触发 - 若接口 P99 响应时间达 2s,
QueueLimit设太高会导致后续请求在队列里等满 30s 才被拒绝,应设为Math.Min(5, (int)(30 / avgResponseSeconds))
漏桶算法在 C# 里其实很少单独用
漏桶强调恒定输出速率,天然适合做“削峰填谷”,但 .NET 生态中几乎没有开箱即用的漏桶限流器。Microsoft.Extensions.RateLimiting 不提供漏桶实现,第三方库如 AspNetCoreRateLimit 也只支持滑动窗口或固定窗口。真要模拟漏桶,得自己维护一个按固定间隔出队的 ConcurrentQueue + Timer,但会引入时钟漂移、GC 暂停导致漏速不准等问题。实际项目中,更常见的是用消息队列(如 RabbitMQ 的 x-max-length + x-overflow=reject-publish)在网关层做漏桶语义。
分布式场景下令牌桶怎么保持一致性
单机 TokenBucketRateLimiter 默认用内存存储,集群部署时必须切换为分布式模式。关键不是换存储,而是选对序列化方式:IDistributedCache 存的是 TokenBucketState 结构体,含 AvailableTokens、LastReplenishedUtc 等字段。Redis 是首选,但要注意:StackExchange.Redis 的 StringGetAsync + StringSetAsync 必须用 Lua 脚本保证原子性,否则多个实例可能同时读到旧值并各自填充,导致超发。官方限流器已内置该脚本,只需确认你用的是 RedisRateLimiter(.NET 8+)或配置了 AddDistributedRateLimiter 并注入 RedisCache 实例。
services.AddRateLimiter(options =>
{
options.AddPolicy("api", context =>
context.Request.RouteValues["controller"]?.ToString() == "Api"
? new TokenBucketRateLimiterOptions
{
PermitLimit = 10,
TokensPerPeriod = 10,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
QueueLimit = 3
}
: new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1) });
});真正难的不是配参数,而是理解“令牌桶”本质是个带状态的时间函数——它把请求速率映射成一个可加减的整数,而这个整数在分布式环境下必须靠强一致存储兜底。多数人卡在 Redis 连接超时没重试、缓存键没加前缀导致多服务冲突、或者误把 QueueLimit 当作总请求数限制。










