Go微服务限流首选单机rate.Limiter,但分布式场景需go-zero的Redis滑动窗口限流;须按用户/API维度区分、配置热更新、限流前置、控制等待超时,并在跨服务调用中实施出向限流。

Go 微服务中用 golang.org/x/time/rate 做请求限流
标准库 rate.Limiter 是 Go 里最轻量、最常用、也最容易误用的限流方案。它适合单机粒度的 QPS 控制,比如限制某个 HTTP handler 每秒最多处理 100 个请求。
关键点在于:它不跨进程、不跨机器,也不感知上游负载;只靠本地 ticker 和 token bucket 实现,无锁且低开销。
-
rate.NewLimiter(rate.Limit(100), 10)表示最大允许 100 QPS,初始桶容量为 10(即突发最多允许 10 个请求立刻通过) - 在 handler 中调用
limiter.Allow()或limiter.Wait(ctx)—— 后者会阻塞直到有 token,前者立即返回 bool - 别把同一个
Limiter实例反复 new,应作为包变量或依赖注入共享;否则每个请求都新建,限流完全失效
func handleRequest(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
// 处理业务逻辑
}
为什么 go-zero 的 rest.Middleware 限流更实用
真实微服务中,单机限流往往不够:你可能要按用户 ID、API 路径、租户标识做区分限流,还要支持配置热更新和集群协同。这时候直接手写 rate.Limiter 就力不从心了。
go-zero 提供的 rest.Middleware 内置了基于滑动窗口 + Redis 的分布式限流器,同时保留了本地 fallback 能力(Redis 不可用时自动降级为单机限流)。
立即学习“go语言免费学习笔记(深入)”;
- 配置项如
limit: 200,interval: 60对应“每分钟最多 200 次” - 需配合
redis地址和 key 前缀使用,key 自动生成(含 path + 用户标识等),避免手动拼接出错 - 注意中间件注册顺序:限流中间件必须在鉴权之后、业务 handler 之前,否则无法获取
user_id等上下文用于维度限流
遇到 context deadline exceeded 时,别急着调大超时
限流器本身不会导致超时,但常见错误是把限流逻辑放在耗时操作之后(比如先查 DB、再限流),或者在限流等待时没传入带 timeout 的 context。
- 正确做法:限流判断必须是 handler 最早执行的逻辑之一,且
limiter.Wait(ctx)的ctx应来自r.Context(),而非context.Background() - 如果用了
Wait但上游已设 5s timeout,而限流桶空了,就会卡满 5s 再报错——这不是限流失效,而是你没控制好等待上限 - 更稳妥的是用
TryConsume+ 自定义排队队列,或改用带最大等待时间的封装(如limiter.WaitN(ctx, 1, time.Second))
跨服务调用链路上的流量控制不能只靠入口限流
一个典型场景:A 服务限流 100 QPS,但它调用 B 服务的某个接口,而 B 服务没做任何保护。结果 A 的限流形同虚设,B 直接被打挂。
这时需要在客户端(A)侧对下游(B)做「出向限流」,而不是只守入口:
- 用
google.golang.org/grpc/metadata透传限流上下文(如req_id,tenant_id),让 B 能做多维决策 - A 调用 B 前,用本地
rate.Limiter控制对 B 的调用频次(例如每秒最多发 50 个 RPC) - B 收到请求后,优先校验元数据中的限流标识,再决定是否走分布式限流器,避免被穿透
真正难的不是写几行限流代码,而是厘清「谁限谁」「按什么维度限」「失败时怎么降级」——这些逻辑一旦散落在各处,很快就会变成线上事故的温床。










