接口限流策略

2026-06-22 · 6 阅读 · 359字
Go安全微服务

接口限流策略

为什么需要限流

限流是保护后端服务稳定性的重要手段。当流量超出系统处理能力时,限流可以:

  1. 防止服务雪崩:避免单个服务过载影响整个系统
  2. 公平分配资源:确保所有用户都能获得服务
  3. 防范恶意攻击:抵御 DDoS 和暴力破解
  4. 成本控制:控制 API 调用量,避免资源过度消耗

常见限流算法

计数器算法

最简单的实现,在固定时间窗口内计数:

type CounterLimiter struct {
    limit    int
    window   time.Duration
    requests map[string]*counter
}

func (l *CounterLimiter) Allow(key string) bool {
    now := time.Now()
    c, ok := l.requests[key]
    if !ok || now.Sub(c.windowStart) > l.window {
        l.requests[key] = &counter{windowStart: now, count: 1}
        return true
    }
    if c.count < l.limit {
        c.count++
        return true
    }
    return false
}

缺点:存在临界突变问题——窗口边界处的流量突增可能超过限制的两倍。

滑动窗口算法

将时间窗口细分为多个小格子,更精确地控制流量:

type SlidingWindowLimiter struct {
    limit    int
    window   time.Duration
    slots    int
    requests map[string]*sync.Map
}

滑动窗口通过记录每个时间戳的请求,解决了计数器算法的边界突变问题。

令牌桶算法

以固定速率向桶中添加令牌,请求需获取令牌才能通过:

type TokenBucket struct {
    rate     float64    // 令牌添加速率(个/秒)
    capacity int64      // 桶容量
    tokens   int64      // 当前令牌数
    lastTime time.Time  // 上次更新令牌的时间
    mu       sync.Mutex
}

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.capacity, tb.tokens + int64(elapsed * tb.rate))
    tb.lastTime = now

    if tb.tokens > 0 {
        tb.tokens--
        return true
    }
    return false
}

优点:允许突发流量(桶中积累的令牌),同时限制平均速率。

漏桶算法

请求以固定速率被处理,超出桶容量的请求被丢弃:

        请求进入
           ↓
    ┌──────────────┐
    │   漏桶(队列)  │  容量 = N
    └──────────────┘
           ↓(恒定速率)
         处理请求

优点:输出流量完全平滑,适合保护下游系统。 缺点:无法利用空闲资源处理突发流量。

分布式限流

在分布式系统中,限流计数器需要共享存储:

Redis 实现

func AllowRequest(userID string, limit int, window time.Duration) bool {
    key := fmt.Sprintf("rate_limit:%s:%d", userID, window.Seconds())
    current, _ := redis.Incr(key)
    if current == 1 {
        redis.Expire(key, window)
    }
    return current <= limit
}

使用 Redis 的 INCR + EXPIRE 实现滑动窗口,或用 ZSET 实现精确滑动窗口:

func SlidingWindowAllow(userID string, limit int, window time.Duration) bool {
    key := "rate_limit:" + userID
    now := time.Now().UnixMilli()
    windowStart := now - window.Milliseconds()

    // 移除窗口外的记录
    redis.ZRemRangeByScore(key, "0", strconv.FormatInt(windowStart, 10))

    // 统计窗口内的请求数
    count, _ := redis.ZCard(key)

    if count < limit {
        redis.ZAdd(key, &redis.Z{Score: float64(now), Member: now})
        redis.Expire(key, window)
        return true
    }
    return false
}

限流策略配置

分级限流

rate_limits:
  # 用户级别
  user:
    free:    10/minute, 100/hour
    pro:     100/minute, 1000/hour
    enterprise: 1000/minute, unlimited/hour

  # API 级别
  api:
    /api/login:     5/minute   # 登录接口严格限流
    /api/search:    30/minute  # 搜索接口
    /api/data:      100/minute # 数据接口

  # 全局级别
  global:
    max_connections:   10000
    max_requests:      100000/minute

限流响应

当请求被限流时,返回 429 Too Many Requests:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200

{
    "error": "rate_limit_exceeded",
    "message": "请求过于频繁,请稍后重试",
    "retry_after": 30
}