设计限流器
问题
如何用 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 告诉客户端多久后重试。