time.ticker 不适合接口限流,因其固定窗口机制会漏判突发流量;应使用 rate.limiter(令牌桶)或滑动窗口方案,配合动态 key 管理与 redis+lua 原子操作实现高精度、并发安全的限流。
为什么 time.Ticker 不适合做接口限流
直接用 time.Ticker 配合计数器实现“每秒最多 N 次”看似简单,但实际会漏判突发流量。它只在固定时间点检查累计请求数,两次 tick 之间涌入的请求全被算进下一个窗口,导致瞬时超限。真正要的是滑动窗口或令牌桶这类能连续计量的模型。
实操建议:
- 别自己基于
time.AfterFunc或time.Tick手写重置逻辑,容易出竞态 - 优先选用成熟限流库(如
golang.org/x/time/rate),它的rate.Limiter底层用原子操作+单调时钟,精度和并发安全都有保障 - 若需滑动窗口(比如“最近 60 秒内最多 100 次”),
rate.Limiter默认不支持,得换uber-go/ratelimit或用 Redis + Lua 实现
用 rate.Limiter 做 HTTP 中间件的正确姿势
rate.Limiter 的核心是 Allow() 和 Wait() —— 前者非阻塞判断,后者会阻塞直到有配额。Web 接口通常该用 Allow() 快速失败,避免协程堆积。
常见错误现象:在中间件里对每个请求都调用 limiter.Wait(ctx),结果高并发下大量 goroutine 卡住,内存暴涨甚至 OOM。
实操建议:
- 初始化时按需创建
rate.Limiter,例如rate.NewLimiter(10, 5)表示“平均 10 QPS,最多允许 5 个请求瞬时突增” - 中间件中用
if !limiter.Allow() { http.Error(w, "too many requests", http.StatusTooManyRequests); return } - 别把同一个
rate.Limiter实例全局共享给所有路由——不同接口应有独立配额,按路径或用户 ID 构建 key 做 map 分片
如何按用户 ID 或 IP 做差异化限流
硬编码一个 limiter 只能做全局限流。真实场景需要“每个用户每分钟最多 30 次”,这就得动态管理 limiter 实例,并控制内存增长。