为什么直接用 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当作总请求数限制。
