极简架构的限流算法选型:令牌桶、滑动窗口与自适应限流的性能对比

📅 2026/7/17 16:27:10
极简架构的限流算法选型:令牌桶、滑动窗口与自适应限流的性能对比
极简架构的限流算法选型令牌桶、滑动窗口与自适应限流的性能对比一、同一个接口的三种限流方式效果天差地别一个 API 服务使用固定窗口限流每分钟 100 次。用户在前 2 秒内打满 100 次请求后后 58 秒全部被拒绝——时间窗口的边界效应。在这个 2 秒的流量尖刺中服务虽然没被打垮但数据库的 CPU 飙到了 89%——100 个请求在 2 秒内的瞬时压力大于 100 个请求在 60 秒内的均匀分布。切换到令牌桶算法后同样的流量模式不再触发数据库压力告警——桶的容量限制了突发流量用户请求被平滑地排出而不是在窗口边界集中涌入。二、四种限流算法的对比算法突发容忍实现复杂度内存占用精度适用场景固定窗口高(边界效应)低极低低非关键 API滑动窗口中中中高API 限流(推荐)令牌桶可控(桶容量)中低高允许合理突发漏桶零中低高严格的速率整形2.1 令牌桶的极简实现// 令牌桶允许短期突发长期限定平均速率 type TokenBucket struct { rate float64 // 令牌生成速率个/秒 capacity float64 // 桶容量最大突发 tokens float64 // 当前令牌数 lastUpdate time.Time // 上次更新令牌的时间 mu sync.Mutex } func NewTokenBucket(rate, capacity float64) *TokenBucket { return TokenBucket{ rate: rate, capacity: capacity, tokens: capacity, lastUpdate: time.Now(), } } func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() // 计算流逝时间内新增的令牌 now : time.Now() elapsed : now.Sub(tb.lastUpdate).Seconds() tb.tokens math.Min(tb.capacity, tb.tokens elapsed*tb.rate) tb.lastUpdate now if tb.tokens 1.0 { tb.tokens - 1.0 return true } return false }2.2 滑动窗口的精确实现// 滑动窗口日志存储最近的请求时间戳 type SlidingWindowLog struct { window time.Duration // 窗口大小如 1 分钟 maxReqs int // 窗口内最大请求数 timestamps []time.Time // 请求时间戳链表 mu sync.Mutex } func (sw *SlidingWindowLog) Allow() bool { sw.mu.Lock() defer sw.mu.Unlock() now : time.Now() cutoff : now.Add(-sw.window) // 清理过期的请求记录 valid : 0 for _, ts : range sw.timestamps { if ts.After(cutoff) { sw.timestamps[valid] ts valid } } sw.timestamps sw.timestamps[:valid] if len(sw.timestamps) sw.maxReqs { return false } sw.timestamps append(sw.timestamps, now) return true }2.3 自适应限流// 自适应限流根据系统负载动态调整速率 type AdaptiveLimiter struct { bucket *TokenBucket cpuThreshold float64 // CPU 阈值超过即降低速率 minRate float64 maxRate float64 } func (l *AdaptiveLimiter) Allow() bool { // 获取当前 CPU 使用率 cpuUsage : getCPUUsage() // CPU 高时降低速率CPU 低时恢复 currentRate : l.maxRate if cpuUsage l.cpuThreshold { ratio : (1.0 - cpuUsage) / (1.0 - l.cpuThreshold) currentRate l.minRate (l.maxRate - l.minRate)*ratio if currentRate l.minRate { currentRate l.minRate } } l.bucket.rate currentRate return l.bucket.Allow() }三、性能基准对比在 8 核 CPU 上对 100 万次Allow()调用进行基准测试算法ns/op内存/op分配次数/op固定窗口12 ns0 B0令牌桶45 ns0 B0滑动窗口日志180 ns80 B1自适应限流55 ns0 B0固定窗口最快但精度最差滑动窗口最精确但需要内存分配存储时间戳。令牌桶在速度和精度之间取得了最好的平衡。四、边界与权衡分布式限流的复杂性单机限流容易分布式限流需要同步状态。Redis 的INCREXPIRE实现固定窗口很简单但令牌桶在分布式环境下的同步开销显著。建议单机使用令牌桶分布式使用 Redis 固定窗口 本地令牌桶双层限流。令牌桶的突发副作用桶容量 100 意味着在令牌攒满后可以瞬间发出 100 个请求。如果下游服务无法承受这个突发量需要调低桶容量。过于激进的限流限流阈值设置过低会让正常用户被误杀。建议从宽松阈值开始预测峰值 QPS 的 2 倍逐步收紧每个阶段观察业务指标订单量、用户投诉。五、总结API 限流算法选型微服务内部调用用令牌桶平衡突发和平均、面向用户 API 用滑动窗口精确公平、系统保护用自适应限流按系统负载动态调整。令牌桶是 90% 场景下的最优默认选择——实现简单、性能高、允许合理的请求突发。实施建议先上固定窗口验证限流逻辑是否正确 → 切换为令牌桶调优突发容量→ 在流量稳态后加入自适应降速。限流算法不是一劳永逸的——根据流量模式的演变日均 QPS、峰值/均值比、下游承受能力定期复审参数。