★ 令牌桶限流细节: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。