在高并发系统中,接口限流是保护服务稳定性的重要手段。本文深入讲解三种经典限流算法——令牌桶(Token Bucket)、漏桶(Leaky Bucket)和滑动窗口(Sliding Window),并提供完整的Go语言实现。

一、为什么需要限流?

在分布式系统中,突发流量可能导致服务过载甚至崩溃。限流(Rate Limiting)的核心目标是:在有限资源下,控制请求的处理速率,保障服务稳定运行。常见的应用场景包括:

  • 防止恶意攻击或爬虫过度消耗资源
  • 保护下游服务不被突发流量击垮
  • 实现API的QoS分级,保障核心接口可用性
  • 防止数据库连接池耗尽

二、固定窗口计数器(Fixed Window Counter)

这是最简单的限流算法:在固定时间窗口内统计请求数,超过阈值则拒绝请求。

package main

import (
    "fmt"
    "sync"
    "time"
)

// FixedWindowLimiter 固定窗口限流器
type FixedWindowLimiter struct {
    rate      int           // 窗口内允许的最大请求数
    interval  time.Duration // 窗口大小
    count     int           // 当前窗口请求计数
    windowEnd time.Time     // 窗口结束时间
    mu        sync.Mutex
}

func NewFixedWindowLimiter(rate int, interval time.Duration) *FixedWindowLimiter {
    return &FixedWindowLimiter{
        rate:      rate,
        interval:  interval,
        windowEnd: time.Now().Add(interval),
    }
}

func (f *FixedWindowLimiter) Allow() bool {
    f.mu.Lock()
    defer f.mu.Unlock()

    now := time.Now()
    // 如果当前时间超过窗口结束时间,重置窗口
    if now.After(f.windowEnd) {
        f.count = 0
        f.windowEnd = now.Add(f.interval)
    }

    if f.count >= f.rate {
        return false
    }
    f.count++
    return true
}

func main() {
    limiter := NewFixedWindowLimiter(5, time.Second)

    for i := 0; i < 10>

缺陷:固定窗口存在"临界问题"——在窗口边界处可能出现两倍的限流阈值。例如:1秒允许100请求,第0.9秒来了100个请求,第1.0秒重置后又来了100个请求,实际上0.2秒内通过了200个请求。

三、滑动窗口(Sliding Window)

滑动窗口算法将时间划分为多个小窗口,随着时间推移,窗口向前滑动。通过统计多个小窗口的请求总数来判断是否限流,有效解决了固定窗口的临界问题。

package main

import (
    "container/list"
    "fmt"
    "sync"
    "time"
)

// SlidingWindowLimiter 滑动窗口限流器
type SlidingWindowLimiter struct {
    rate       int           // 允许的最大请求数
    windowSize time.Duration // 滑动窗口总大小
    interval   time.Duration // 每个子窗口大小
    requests   *list.List    // 存储请求时间戳
    mu         sync.Mutex
}

func NewSlidingWindowLimiter(rate int, windowSize time.Duration, subWindows int) *SlidingWindowLimiter {
    return &SlidingWindowLimiter{
        rate:       rate,
        windowSize: windowSize,
        interval:   windowSize / time.Duration(subWindows),
        requests:   list.New(),
    }
}

func (s *SlidingWindowLimiter) Allow() bool {
    s.mu.Lock()
    defer s.mu.Unlock()

    now := time.Now()
    // 移除窗口外的所有请求记录
    cutoff := now.Add(-s.windowSize)
    for s.requests.Len() > 0 {
        front := s.requests.Front()
        if front.Value.(time.Time).Before(cutoff) {
            s.requests.Remove(front)
        } else {
            break
        }
    }

    // 检查当前窗口内的请求数是否超限
    if s.requests.Len() >= s.rate {
        return false
    }

    s.requests.PushBack(now)
    return true
}

func main() {
    // 1秒内最多50个请求,分为10个子窗口
    limiter := NewSlidingWindowLimiter(50, time.Second, 10)

    passed, rejected := 0, 0
    for i := 0; i < 60>

四、漏桶算法(Leaky Bucket)

漏桶算法以水做类比:请求像水一样流入桶中,桶以固定速率漏水(处理请求)。如果水流入速度过快(请求过多),桶满后水会溢出(请求被拒绝)。

特点:输出速率恒定,能将突发流量整形为匀速输出,适合下游处理能力固定的场景。

package main

import (
    "fmt"
    "sync"
    "time"
)

// LeakyBucketLimiter 漏桶限流器
type LeakyBucketLimiter struct {
    capacity   int           // 桶容量
    rate       float64       // 漏水速率(每秒处理请求数)
    water      float64       // 当前桶中水量
    lastLeak   time.Time     // 上次漏水时间
    mu         sync.Mutex
}

func NewLeakyBucketLimiter(capacity int, rate float64) *LeakyBucketLimiter {
    return &LeakyBucketLimiter{
        capacity: capacity,
        rate:     rate,
        lastLeak: time.Now(),
    }
}

func (l *LeakyBucketLimiter) Allow() bool {
    l.mu.Lock()
    defer l.mu.Unlock()

    now := time.Now()
    // 计算自上次漏水以来流出的水量
    elapsed := now.Sub(l.lastLeak).Seconds()
    leaked := elapsed * l.rate
    l.water -= leaked
    if l.water < 0 xss=removed xss=removed> float64(l.capacity) {
        return false
    }

    l.water++
    return true
}

func main() {
    // 桶容量20,每秒处理5个请求
    limiter := NewLeakyBucketLimiter(20, 5)

    // 模拟突发流量
    fmt.Println("=== 突发流量测试 ===")
    for i := 0; i < 10>

五、令牌桶算法(Token Bucket)

令牌桶算法是实践中最常用的限流算法。系统以固定速率向桶中添加令牌,每个请求需要消耗一个令牌。如果桶为空且令牌不足,请求被拒绝。

特点:允许突发流量(只要桶中有足够的令牌),同时限制了平均速率。这是API网关(如Redis的redis-cell模块)常用的算法。

package main

import (
    "fmt"
    "sync"
    "time"
)

// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
    capacity   int           // 桶最大容量
    rate       float64       // 令牌生成速率(每秒)
    tokens     float64       // 当前令牌数
    lastRefill time.Time     // 上次补充令牌时间
    mu         sync.Mutex
}

func NewTokenBucketLimiter(capacity int, rate float64) *TokenBucketLimiter {
    return &TokenBucketLimiter{
        capacity:   capacity,
        rate:       rate,
        tokens:     float64(capacity), // 初始满桶
        lastRefill: time.Now(),
    }
}

func (t *TokenBucketLimiter) Allow() bool {
    return t.AllowN(1)
}

// AllowN 尝试消耗n个令牌
func (t *TokenBucketLimiter) AllowN(n int) bool {
    t.mu.Lock()
    defer t.mu.Unlock()

    t.refill()

    if float64(n) > t.tokens {
        return false
    }

    t.tokens -= float64(n)
    return true
}

// refill 补充令牌
func (t *TokenBucketLimiter) refill() {
    now := time.Now()
    elapsed := now.Sub(t.lastRefill).Seconds()
    tokensToAdd := elapsed * t.rate
    t.tokens += tokensToAdd
    if t.tokens > float64(t.capacity) {
        t.tokens = float64(t.capacity)
    }
    t.lastRefill = now
}

// Wait 等待直到获取令牌,返回等待时间
func (t *TokenBucketLimiter) Wait(n int) time.Duration {
    t.mu.Lock()
    defer t.mu.Unlock()

    t.refill()

    if t.tokens >= float64(n) {
        t.tokens -= float64(n)
        return 0
    }

    // 计算需要等待的时间
    deficit := float64(n) - t.tokens
    waitTime := time.Duration(deficit/t.rate*1000) * time.Millisecond
    t.tokens = 0
    t.lastRefill = t.lastRefill.Add(waitTime)
    return waitTime
}

func main() {
    // 桶容量50,每秒生成10个令牌
    limiter := NewTokenBucketLimiter(50, 10)

    // 测试突发消费
    fmt.Println("=== 令牌桶突发消费测试 ===")
    for i := 0; i < 15>

六、三种算法对比与选型建议

算法优点缺点适用场景
滑动窗口平滑过渡、无临界问题内存开销较高需要精确限流的场景
漏桶输出速率恒定无法应对突发流量下游处理能力固定的场景(如数据库写入)
令牌桶允许突发流量参数调优复杂API网关、大多数Web服务

七、生产环境最佳实践

在实际工程中,限流通常部署在API网关层(如Kong、Nginx、APISIX),建议关注以下几点:

  1. 分布式限流:多实例部署时使用Redis实现集群级限流,例如用Lua脚本实现原子化令牌桶
  2. 多维度限流:同时按IP、用户ID、API路径等维度设置不同限流规则
  3. 熔断配合:限流不是万能的,应与熔断器(Circuit Breaker)、降级策略配合使用
  4. 监控告警:对限流触发次数进行监控,发现异常流量及时告警

八、总结

限流算法本质是在"系统吞吐"与"服务保护"之间做权衡。滑动窗口提供精确的速率控制,漏桶实现流量整形,令牌桶则在限流的同时保留了一定的突发处理能力。理解各算法的原理与适用场景,才能在实际架构设计中做出正确的选型。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论