限流中间件:令牌桶接入 Gin

结论先行

Gin 不内置限流。单实例最常用 Go 官方扩展库 golang.org/x/time/rate 实现令牌桶rate.NewLimiter(rate.Limit(rps), burst),中间件里 if !limiter.Allow() { 429 } 否则放行。rps 是每秒补充的令牌数(平均速率),burst 是桶容量(瞬时突发上限)。多实例/分布式要改用 Redis 等集中式方案,单机内存计数会各节点不一致。

令牌桶参数含义

参数含义示例
rate.Limit(n)每秒补充 n 个令牌 = 平均 QPS100
burst桶容量,允许的瞬时突发请求数200(100/s,可扛 200 突发)
Allow()取 1 个令牌,没有则返回 false立即返回,不做排队

补充:漏桶(恒定速率输出、平滑)适合“绝对匀速”;令牌桶允许突发更符合 Web API。Allow 是即时判断;要排队等待可用 Wait/WaitN

代码示例(每 IP 一个限流器)

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

type ipLimiter struct {
    lim  *rate.Limiter
    last time.Time
}

var (
    mu       sync.Mutex
    limiters = map[string]*ipLimiter{} // 注意:要清理,防内存泄漏
)

func getLimiter(ip string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    if l, ok := limiters[ip]; ok {
        l.last = time.Now()
        return l.lim
    }
    l := &ipLimiter{lim: rate.NewLimiter(rate.Limit(10), 20), last: time.Now()}
    limiters[ip] = l
    return l.lim
}

func RateLimit() gin.HandlerFunc {
    return func(c *gin.Context) {
        if !getLimiter(c.ClientIP()).Allow() {
            c.Header("Retry-After", "1")
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
            return
        }
        c.Next()
    }
}

生产注意

  • 每 IP/每用户建限流器要带 TTL 清理(上例 last 字段配合定时任务删除过期项),否则 map 无限膨胀。
  • 全局限流(防整体被打爆)与按用户限流(防单用户刷)通常是两个中间件/两个 key。
  • 依赖 c.ClientIP 做 key 时先解决代理信任问题(见第 25 章),否则伪造 X-Forwarded-For 即可绕过。
  • 分布式场景:Redis 令牌桶/滑动窗口(Lua 实现或 redis-cell 模块)或交给 API 网关统一限流。
  • 429 响应带上 Retry-After,客户端才知道何时重试。

常见追问 / 记忆点

  • 追问:令牌桶和漏桶区别?答:令牌桶允许突发(桶里攒的令牌一次花完),漏桶输出速率恒定,把突发抹平。
  • 追问:为什么单机 map 限流在集群下失效?答:每个实例各自计数,总量变成 n 倍;需要集中式存储。
  • 记忆点:x/time/rate = 令牌桶;Allow 失败回 429;按 key 限流记得清理、集群用 Redis。
笔记加载中…