限流中间件:golang.org/x/time/rate 令牌桶接入 Gin

限流保护后端不被突发流量打爆。最常用的扩展包是 golang.org/x/time/rate,它实现令牌桶:按固定速率向桶里放令牌,每次请求取一枚,桶满则拒绝。安装:go get golang.org/x/time/rate

1. 令牌桶基本用法

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

// 每秒放 5 枚令牌,桶容量 10(允许最多 10 个瞬时突发)
limiter := rate.NewLimiter(rate.Limit(5), 10)

limiter.Allow()   // 立即取一枚,不够返回 false(不阻塞)
limiter.Wait(ctx) // 阻塞等待直到有令牌,ctx 取消则返回错误

Allow 适合做成「快速失败」返回 429;Wait 适合平滑排队。

2. 按客户端 IP 限流的中间件

每 IP 一个 limiter,需要一个带锁的 map 做查找:

type ipLimiter struct {
    mu      sync.Mutex
    limiters map[string]*rate.Limiter
}

func (l *ipLimiter) get(ip string) *rate.Limiter {
    l.mu.Lock()
    defer l.mu.Unlock()
    lim, ok := l.limiters[ip]
    if !ok {
        lim = rate.NewLimiter(rate.Limit(10), 20) // 每 IP 每秒 10 次
        l.limiters[ip] = lim
    }
    return lim
}

func RateLimit() gin.HandlerFunc {
    lim := &ipLimiter{limiters: make(map[string]*rate.Limiter)}
    return func(c *gin.Context) {
        if !lim.get(c.ClientIP()).Allow() {
            c.AbortWithStatusJSON(http.StatusTooManyRequests,
                gin.H{"error": "请求过于频繁,请稍后再试"})
            return
        }
        c.Next()
    }
}

3. 挂载与验证

r := gin.Default()
r.Use(RateLimit()) // 全局限流
r.GET("/api/search", SearchHandler)

用 curl 连续请求 /api/search,超过桶容量后应看到 HTTP/1.1 429

4. 按用户或接口维度限流

思路不变,只是 key 从 IP 换成用户 ID 或「接口名+用户」:

  • 登录接口:按 IP 限,防撞库;
  • 下单接口:按 userID 限,防刷单;
  • 注意 map 会无限增长:定期清理或改用 LRU 缓存。

注意点

  • map 非并发安全,任何读写都要加锁。
  • 限流状态在内存里,多实例部署各自独立;要全局生效需分布式限流(Redis 滑动窗口/令牌桶)。
  • 429 响应建议带 Retry-After 头,提示客户端何时重试。
  • 别把 IP 当唯一标识:NAT 后多用户共享 IP 会被误伤,可叠加账号维度。

小结

单机限流 = rate.NewLimiter(速率, 容量) + 每 key 一个实例 + 429 快速失败。先想清限的维度(IP/用户/接口),再决定用 Allow 还是 Wait,压测调参时注意观察误杀率。

笔记加载中…