Go 限流

Go 服务中的限流方案,涵盖令牌桶与漏桶算法、golang.org/x/time/rate、HTTP 中间件集成与分布式限流。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

Go 限流

一句话定义

Go 限流是通过令牌桶或漏桶算法控制请求速率,保护服务不被过载的防御机制,标准库扩展包 golang.org/x/time/rate 提供了开箱即用的实现。

核心机制 / 工作原理

令牌桶(Token Bucket)算法

  • 桶以固定速率 r 产生令牌,桶容量为 b(突发上限)。
  • 每个请求消耗一个令牌,桶空则拒绝或等待。
  • 允许突发流量(桶满时可瞬间消耗 b 个请求),但长期速率不超过 r

漏桶(Leaky Bucket)算法

  • 请求进入桶(队列),桶以固定速率处理。
  • 桶满则拒绝新请求。
  • 输出速率恒定,完全平滑流量。

两者核心区别:令牌桶允许突发,漏桶强制平滑。多数场景推荐令牌桶。

golang.org/x/time/rate(标准实现)

import "golang.org/x/time/rate"

// 每秒 100 个请求,突发最多 200 个
limiter := rate.NewLimiter(100, 200)

// 等待直到有令牌(阻塞)
err := limiter.Wait(ctx)

// 尝试获取令牌(非阻塞)
if !limiter.Allow() {
    http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
    return
}

// 预留令牌(带超时)
ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel()
if err := limiter.WaitN(ctx, 1); err != nil {
    http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
    return
}

HTTP 中间件集成

func RateLimitMiddleware(limiter *rate.Limiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                w.Header().Set("Retry-After", "1")
                http.Error(w, "too many requests", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 使用
limiter := rate.NewLimiter(100, 200)
mux := http.NewServeMux()
mux.Handle("/api/", RateLimitMiddleware(limiter)(apiHandler))

Per-User 限流

type UserRateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.Mutex
    rate     rate.Limit
    burst    int
}

func NewUserRateLimiter(r rate.Limit, burst int) *UserRateLimiter {
    return &UserRateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rate:     r,
        burst:    burst,
    }
}

func (l *UserRateLimiter) GetLimiter(userID string) *rate.Limiter {
    l.mu.Lock()
    defer l.mu.Unlock()

    if lim, exists := l.limiters[userID]; exists {
        return lim
    }
    lim := rate.NewLimiter(l.rate, l.burst)
    l.limiters[userID] = lim
    return lim
}

// 使用
userLimiter := NewUserRateLimiter(10, 20) // 每用户每秒 10 个,突发 20
limiter := userLimiter.GetLimiter(getUserID(r))
if !limiter.Allow() {
    http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
}

注意:内存在增长,生产环境需要定期清理不活跃用户的 Limiter(LRU 或定时扫描)。

分布式限流(Redis)

// Redis 滑动窗口限流(Lua 脚本保证原子性)
const luaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, now .. math.random())
    redis.call('EXPIRE', key, window / 1000)
    return 1
end
return 0
`

script := redis.NewScript(luaScript)
allowed, _ := script.Run(ctx, rdb, []string{"rate:user:" + userID}, 100, 60000, time.Now().UnixMilli()).Int()
if allowed == 0 {
    http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
}

最小例子 / 最小场景

// 全局限流:每秒 50 个请求,允许突发 100 个
limiter := rate.NewLimiter(50, 100)

mux := http.NewServeMux()
mux.HandleFunc("/api/resource", func(w http.ResponseWriter, r *http.Request) {
    if !limiter.Allow() {
        http.Error(w, "too many requests", http.StatusTooManyRequests)
        return
    }
    w.Write([]byte("OK"))
})

为什么重要

  • 服务保护:防止恶意流量或突发流量压垮服务,维持可用性。
  • 公平性:Per-User 限流防止单个用户占用全部资源。
  • 成本控制:第三方 API(如支付网关)通常有调用频率限制,客户端限流可避免触发对方限制。
  • 优雅降级:限流后返回 429 而非让服务崩溃或 OOM,是更好的用户体验。

边界与易混淆点

  • 全局 vs Per-User:全局限流保护服务总容量,Per-User 限流保护公平性。两者应组合使用。
  • rate.Limiter 是有状态的:内部维护令牌数量和最后更新时间,不能序列化到 Redis。分布式限流需要 Redis Lua 脚本或专用方案(如 go-redis/redis_rate)。
  • Burst 的含义rate.NewLimiter(100, 200)200 是桶容量,不是”每秒允许 200 个”。它允许瞬间消耗 200 个令牌,之后恢复到每秒 100 个。
  • 内存泄漏:Per-User 限流的 Map 会持续增长。必须加淘汰机制:LRU 缓存、TTL 过期、或定期清理。
  • 429 响应头:应返回 Retry-After 头告知客户端何时可以重试,配合指数退避(Exponential Backoff)效果最佳。
  • 限流 vs 熔断:限流控制请求速率(输入端),熔断控制下游调用失败率(输出端)。两者解决不同问题,通常同时使用。
创建于 2026/6/25 更新于 2026/6/25