服务被突发流量或爬虫打满,正常用户也进不来。限流在入口挡掉超额请求,保护自身和下游。这一篇做两层:单机令牌桶兜日常,分布式令牌桶兜多实例部署。
1. 令牌桶原理
桶里按固定速率放令牌,请求来取一枚,取不到就拒。它允许短时突发(桶里有存量),比固定窗口平滑。Go 标准扩展库自带实现。
2. 单机中间件
import "golang.org/x/time/rate" func RateLimit(rps float64, burst int) gin.HandlerFunc { limiter := rate.NewLimiter(rate.Limit(rps), burst) return func(c *gin.Context) { if !limiter.Allow() { c.JSON(429, gin.H{"error": "请求过于频繁"}) c.Abort() return } c.Next() } }
rps 是每秒放令牌数,burst 是桶容量(允许的突发量)。Allow() 无阻塞,超了立即返 429。
3. 按用户限流更公平
全局限流会被一个用户占满。用 limiter map 按 uid 隔离,带懒清理:
var users = &sync.Map{} func RateLimitPerUser(rps float64, burst int) gin.HandlerFunc { return func(c *gin.Context) { uid, _ := c.Get("uid") key := fmt.Sprint(uid) v, _ := users.LoadOrStore(key, rate.NewLimiter(rate.Limit(rps), burst)) if !v.(*rate.Limiter).Allow() { c.JSON(429, gin.H{"error": "请求过于频繁"}) c.Abort() return } c.Next() } }
匿名请求按 IP 当 key,登录用户按 uid,避免单 IP 下多用户互相挤。
4. 分布式限流
多实例部署时每个进程各自一个桶,总限额被实例数放大。用 Redis 做全局令牌桶,lua 脚本保证原子:
-- KEYS[1]=limiter key, ARGV[1]=now ms, ARGV[2]=rate, ARGV[3]=burst
local tokens = tonumber(redis.call('get', KEYS[1]) or ARGV[3])
local ts = tonumber(redis.call('get', KEYS[1]..':ts') or ARGV[1])
local delta = math.min(ARGV[3], tokens + (ARGV[1]-ts)/1000*ARGV[2])
if delta < 1 then return 0 end
redis.call('set', KEYS[1], delta-1)
redis.call('set', KEYS[1]..':ts', ARGV[1])
return 1
返回 1 放行、0 拒绝。lua 在 Redis 单线程执行,多实例并发不会超发。
5. Go 侧调用
func allow(key string, rps, burst float64) bool { now := float64(time.Now().UnixMilli()) res, _ := rdb.Eval(ctx, luaScript, []string{key}, now, rps, burst).Int() return res == 1 }
key 拼上用户或 IP,所有实例共享同一计数。
6. 返回标准限流头
告诉客户端还剩多少额度:
c.Header("X-RateLimit-Limit", fmt.Sprint(burst)) c.Header("Retry-After", "1")
前端拿到 429 后按 Retry-After 退避,别死循环重试。
7. 上线清单
限流阈值按压测定,别拍脑袋。全局限流按实例数摊薄。分布式限流 key 设计要能识别用户又不泄隐私。被限流的请求返 429 而非 500,别污染错误监控。静态资源和健康检查不限流。
下一篇讲熔断和重试,保护对下游的调用。