跳到主要内容

设计限流器

问题

如何用 Go 设计一个支持多种策略的限流器?

答案

令牌桶(标准库)

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

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

// 阻塞等待令牌
limiter.Wait(ctx)

// 非阻塞检查
if limiter.Allow() {
// 处理请求
}

// 预留令牌
reservation := limiter.Reserve()
time.Sleep(reservation.Delay())

滑动窗口(Redis)

func SlidingWindowRateLimit(ctx context.Context, rdb *redis.Client, key string, limit int64, window time.Duration) bool {
now := time.Now().UnixMilli()
windowStart := now - window.Milliseconds()

pipe := rdb.Pipeline()
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
countCmd := pipe.ZCard(ctx, key)
pipe.Expire(ctx, key, window)
pipe.Exec(ctx)

return countCmd.Val() <= limit
}

中间件集成

// 按 IP 限流的 Gin 中间件
func RateLimitByIP(rps int) gin.HandlerFunc {
limiters := sync.Map{}

return func(c *gin.Context) {
ip := c.ClientIP()
val, _ := limiters.LoadOrStore(ip, rate.NewLimiter(rate.Limit(rps), rps*2))
limiter := val.(*rate.Limiter)

if !limiter.Allow() {
c.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
}
}

策略对比

策略特点适用场景
固定窗口简单,有边界突发问题粗粒度限流
滑动窗口更平滑,精确精确 QPS 控制
令牌桶允许突发API 限流
漏桶恒定速率流量整形

常见面试问题

Q1: 单机限流 vs 分布式限流?

答案

  • 单机x/time/rate,每个实例独立限流
  • 分布式:Redis + Lua 脚本,全局统一限流
  • 单机简单但不精确(10 个实例 × 100/s = 实际可能 1000/s),分布式精确但有网络开销

Q2: 限流被触发后应该怎么响应?

答案:返回 429 Too Many Requests + Retry-After Header 告诉客户端多久后重试。

相关链接