★ 令牌桶限流细节:golang.org/x/time/rate 原理

一句话结论:x/time/rate 实现的是令牌桶:桶容量 burst 决定最大突发,速率 limit(每秒补充多少个 token)决定长期均值。它不是“后台定时器每秒往桶里放 token”,而是惰性记账——每次请求到来时按“距上次操作经过的时间 × 速率”补 token,再扣减,因此零开销时可放行多少完全由时间推导。核心 API 三选一:Allow() 不等待直接返回能否放行(适合网关拒绝);Wait(ctx) 阻塞到可放行或 ctx 取消(适合排队削峰,务必带超时 ctx);Reserve() 预知需要等多久(Delay),可配合 429 + Retry-After 返回给客户端。

API 与语义

API行为适用
lim.Allow() / AllowN(now,n)立即返回是否放行,不等直接拒绝的限流中间件
lim.Wait(ctx) / WaitN(ctx,n)阻塞直到可放行或 ctx 结束内部排队、削峰
lim.Reserve() / ReserveN返回 Reservation,Delay() 给出等待时长算好 429 重试时间或预占
  • 参数关系:NewLimiter(rate.Limit(10), burst)。速率 r=10(每秒 10 个),burst=1 时严格每 100ms 一个;burst=20 时允许瞬间突发 20 个后回落到 10/s。
  • burst 决定瞬时冲击吸收能力,r 决定平均吞吐上限;上线前按压测定值,别拍脑袋。

Gin 中间件示例

// 全局单桶
var limiter = rate.NewLimiter(rate.Limit(100), 100) // 100 QPS,容忍 100 突发

func RateLimit() gin.HandlerFunc {
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.JSON(429, gin.H{"code": 429, "message": "too many requests"})
            c.Abort()
            return
        }
        c.Next()
    }
}

// 排队版:等得起就等,等不起(ctx 超时)再 429
if err := limiter.Wait(c.Request.Context()); err != nil {
    c.JSON(429, ...); c.Abort(); return
}

按 IP/用户分桶

  • map[ip]*rate.Limiter 加读写锁或分片;每桶记录最后访问时间,定时清理过期桶,防 map 无限增长。
  • 多实例部署时内存桶不共享,需中心化(Redis 固定窗口/滑动窗口、redis-cell)或依赖网关限流;rate 的定位是单进程内精确限速。

追问记忆点

  • 追问:令牌桶和漏桶区别?——令牌桶允许突发(积攒的 token 一次用完);漏桶强制恒定速率输出,x/time/rate 是令牌桶。
  • 追问:burst 设 1 和设 100 分别防什么?——1 是严格匀速防毛刺;100 允许瞬时突发,适合“均值受限、容忍峰值”的业务。
  • 追问:Wait 为什么会泄漏 goroutine?——不传 ctx 或 ctx 无超时,请求方一直等不到 token 就一直挂着;生产必带超时 ctx。
  • 记忆点:令牌桶=burst 突发 + rate 均值;惰性记账无定时器;Allow 拒绝 / Wait 排队(带 ctx)/ Reserve 算 Retry-After。
笔记加载中…