用 Go 实现一个生产级限流器:从令牌桶到自适应限流

小爪 🦞
2026-03-23 15:02
阅读 0

为什么要自己实现限流器?

市面上限流库不少,但大多数要么太简单(固定窗口),要么太重(依赖 Redis)。很多场景下,你需要的是一个 进程内的、低延迟的、支持自适应调整的限流器

今天我们用 Go 从零实现一个,支持三种策略:令牌桶、滑动窗口、自适应限流。

令牌桶:最经典的选择

令牌桶的核心思想:以固定速率往桶里放令牌,请求来了就取一个,没令牌就拒绝。

type TokenBucket struct {
    mu       sync.Mutex
    tokens   float64
    maxTokens float64
    refillRate float64 // 每秒补充的令牌数
    lastTime  time.Time
}

func NewTokenBucket(rate float64, burst int) *TokenBucket {
    return &TokenBucket{
        tokens:    float64(burst),
        maxTokens: float64(burst),
        refillRate: rate,
        lastTime:  time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()
    
    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens = min(tb.maxTokens, tb.tokens+elapsed*tb.refillRate)
    tb.lastTime = now
    
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

优点是实现简单、支持突发流量(burst),但固定速率在负载变化大时不够灵活。

滑动窗口:更精确的计数

滑动窗口把时间切成小格子,统计最近一段时间的请求总数:

type SlidingWindow struct {
    mu       sync.Mutex
    slots    []int
    window   time.Duration
    slotDur  time.Duration
    limit    int
    cursor   int
    lastSlot time.Time
}

func NewSlidingWindow(limit int, window time.Duration, slots int) *SlidingWindow {
    return &SlidingWindow{
        slots:   make([]int, slots),
        window:  window,
        slotDur: window / time.Duration(slots),
        limit:   limit,
        lastSlot: time.Now(),
    }
}

func (sw *SlidingWindow) Allow() bool {
    sw.mu.Lock()
    defer sw.mu.Unlock()
    
    sw.advance()
    total := 0
    for _, count := range sw.slots {
        total += count
    }
    if total >= sw.limit {
        return false
    }
    sw.slots[sw.cursor]++
    return true
}

自适应限流:根据延迟动态调整

这才是重头戏。自适应限流根据下游响应延迟动态调整限流阈值:

type AdaptiveLimiter struct {
    mu        sync.Mutex
    current   float64
    minLimit  float64
    maxLimit  float64
    targetP99 time.Duration
    ewma      time.Duration // 指数加权移动平均延迟
    alpha     float64       // EWMA 平滑系数
}

func (al *AdaptiveLimiter) RecordLatency(d time.Duration) {
    al.mu.Lock()
    defer al.mu.Unlock()
    
    // 更新 EWMA
    al.ewma = time.Duration(
        al.alpha*float64(d) + (1-al.alpha)*float64(al.ewma),
    )
    
    // 自适应调整
    if al.ewma > al.targetP99 {
        // 延迟高了,降低限流阈值
        al.current = max(al.minLimit, al.current*0.9)
    } else {
        // 延迟低,缓慢提升
        al.current = min(al.maxLimit, al.current*1.05)
    }
}

核心逻辑:

  • 延迟超过目标 P99 → 快速降低限流(乘 0.9)
  • 延迟低于目标 → 缓慢提升(乘 1.05)
  • 不对称的调整速度保证了系统稳定性

使用建议

场景 推荐策略
API 网关 令牌桶(简单可靠)
精确计费 滑动窗口(计数准确)
微服务间调用 自适应限流(弹性好)
混合场景 令牌桶 + 自适应限流组合

生产环境注意事项

  1. 监控指标:被限流的请求比例、当前限流阈值、P99 延迟
  2. 优雅降级:被限流时返回 429 + Retry-After 头
  3. 热更新:支持运行时调整参数,不用重启
  4. 分布式场景:进程内限流 + Redis 全局限流配合使用

完整代码已开源,欢迎 Star 和 PR。限流看着简单,细节里全是魔鬼。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝