golang.org/x/time/rate 是单机限流最稳妥方案,基于令牌桶算法,需复用实例、避免新建,配合超时 context 使用,并支持热更新与可观测性。

用 golang.org/x/time/rate 实现单机限流最稳妥
Go 官方维护的 rate.Limiter 是微服务中做单节点请求速率控制的首选,轻量、无依赖、线程安全。它基于令牌桶算法,能平滑应对突发流量,比计数器法更贴近真实业务节奏。
常见错误是直接在 handler 里 new 一个 rate.Limiter,导致每个请求都新建实例,完全不起作用。必须复用同一个实例,通常挂载到 handler 结构体字段或全局变量中。
-
rate.NewLimiter(rate.Limit(10), 5)表示最大允许 10 QPS,初始桶容量为 5(即最多允许 5 次瞬时突增) - 调用
limiter.Allow()判断是否放行;若需阻塞等待,用limiter.Wait(ctx) - 注意:当服务横向扩容时,该方案只对本机生效,无法跨实例协同——这是设计使然,不是 bug
HTTP 中间件封装限流逻辑要避免 context 泄漏
把限流嵌入 HTTP 流程时,别直接在中间件里写 if !limiter.Allow() { http.Error(...) } 就完事。容易忽略超时控制和错误响应格式统一问题。
尤其要注意 http.Request.Context() 生命周期:如果用了 limiter.Wait(ctx),而客户端提前断开连接,但限流器仍在等令牌,就会卡住 goroutine。务必传入带超时的子 context。
立即学习“go语言免费学习笔记(深入)”;
- 推荐模式:
func rateLimitMiddleware(limiter *rate.Limiter) gin.HandlerFunc { return func(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 200*time.Millisecond) defer cancel() if err := limiter.Wait(ctx); err != nil { c.JSON(429, gin.H{"error": "too many requests"}) c.Abort() return } c.Next() } } - 不要用
time.AfterFunc或全局 timer 做“延后重置”之类的手动调度,会引入竞态和内存泄漏 - 如果用的是
net/http原生路由,注意中间件顺序:限流必须在日志、鉴权之后、业务处理之前
对接 Redis 实现分布式限流得绕开 Lua 脚本陷阱
当服务部署多实例且需全局 QPS 控制时,必须借助 Redis。但直接用 INCR + EXPIRE 两步操作有竞态风险:INCR 成功后 EXPIRE 失败,会导致 key 永久存在。
正确做法是用 Lua 脚本原子执行,但 Go 的 redis.UniversalClient.Eval() 默认不校验脚本 SHA,首次运行慢、后续又可能因 Redis 版本差异导致 EVALSHA 失败回退到 EVAL,引发隐性性能抖动。
- 固定脚本内容,预加载 SHA:
const luaScript = ` local current = redis.call("incr", KEYS[1]) if current == 1 then redis.call("expire", KEYS[1], ARGV[1]) end return current ` // 使用 client.Eval(ctx, luaScript, []string{key}, ttlSeconds) - KEYS 必须唯一对应接口维度,例如
"rate:login:192.168.1.100"(按 IP)或"rate:pay:uid_123"(按用户) - 别把整个限流逻辑压进一个 Lua 脚本里做“自适应降级”,可读性和调试成本陡增;复杂策略建议拆到 Go 层决策,Redis 只做原子计数
限流指标上报和动态配置不能硬编码在代码里
上线后发现某接口被误杀,想临时调高阈值,结果要改代码、发版、重启——这是典型反模式。限流参数必须可热更新,且行为可观测。
最容易被忽略的是“限流生效但没埋点”,导致线上出了问题却查不到是谁被拦了、为什么拦。不要只记录 429 状态码,还要打结构化日志:被限流的 path、client_ip、user_id(脱敏)、当前 rate、桶剩余令牌数。
- 用
viper+ etcd/consul 实现配置热加载,监听/rate/api/v1/order这类路径对应的 limit 值变化 - 暴露
/debug/rateHTTP 接口返回各 limiter 当前状态(如limit=100, burst=20, tokens=17.3),方便运维核对 - 避免用
time.Ticker定期拉取配置,应使用 watch 机制;否则配置变更延迟可能达几十秒
真正难的不是写限流代码,而是定义清楚“谁该被限、限多少、什么时候放开”。比如登录接口要区分未注册手机号和已注册用户,支付回调要区分银行渠道和内部渠道——这些业务语义,没法靠通用组件自动识别。










