直接用 time.sleep 做限流会失效,因其仅阻塞当前 goroutine,无法限制全局单位时间请求数;正确做法是使用 rate.limiter(单机)或 redis+lua(分布式),并按路径、用户等维度合理设计限流 key。

为什么直接用 time.Sleep 做限流会失效
很多人第一反应是“请求来了就 time.Sleep(100 * time.Millisecond)”,但这只在单 goroutine 场景下有效。Go 服务通常是并发处理 HTTP 请求的,每个请求都走独立 goroutine,time.Sleep 只阻塞当前 goroutine,对其他请求完全没影响,根本起不到“限制单位时间请求数”的作用。
真正需要的是共享状态 + 原子计数或时间窗口判断。常见错误还包括:用全局变量加 sync.Mutex 锁住整个计数器——高并发下锁争用严重,QPS 上不去;或者把计数存在内存 map 里但没清理过期 key,导致内存泄漏。
用 golang.org/x/time/rate 实现每秒限流最稳妥
rate.Limiter 是 Go 官方维护的令牌桶实现,轻量、线程安全、支持预热和突发流量控制,生产环境首选。
- 初始化时指定每秒最大请求数(
rate.Every(time.Second / 10)表示 10 QPS)和最大突发量(如 5),突发量允许短时压测或重试,避免误杀 - 在 HTTP handler 中调用
limiter.Wait(r.Context())—— 它会自动阻塞直到拿到令牌,超时返回429 Too Many Requests - 注意不要在中间件里对所有路径统一限流,应按
r.URL.Path或用户 ID 分 key 创建不同rate.Limiter实例,否则登录接口和公开文档接口会被互相拖累
示例关键片段:
// 按路径分桶
var limiters = sync.Map{} // path → *rate.Limiter
<p>func getLimiter(path string) <em>rate.Limiter {
if lim, ok := limiters.Load(path); ok {
return lim.(</em>rate.Limiter)
}
lim := rate.NewLimiter(rate.Every(time.Second/5), 3) // 5 QPS,最多积压 3 个
limiters.Store(path, lim)
return lim
}</p><p>func apiHandler(w http.ResponseWriter, r *http.Request) {
lim := getLimiter(r.URL.Path)
if err := lim.Wait(r.Context()); err != nil {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
// 处理业务逻辑
}</p>
Redis + Lua 脚本适合分布式多实例场景
单机 rate.Limiter 在 Kubernetes 多 Pod 或多服务器部署时无法共享状态,必须上 Redis。但不能简单用 INCR + EXPIRE 两步操作——存在竞态:两个请求同时读到 0,都写入并设过期,导致计数翻倍。
必须用 Lua 脚本保证原子性:
- 脚本接收 key(如
"rate:api:/user/profile:192.168.1.100")、窗口秒数、最大请求数 - 用
redis.call("INCR", key)计数,首次调用时用redis.call("EXPIRE", key, window)设 TTL - 返回当前计数值,由 Go 判断是否超限
Go 端调用示例(使用 github.com/go-redis/redis/v8):
const luaScript = `
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
`
<p>script := redis.NewScript(luaScript)
cnt, err := script.Run(ctx, rdb, []string{key}, windowSec).Int64()
if err != nil || cnt > maxReq {
http.Error(w, "Rate limited", http.StatusTooManyRequests)
return
}</p>
注意:key 要包含能区分调用方的信息(如用户 token hash、IP、AppID),否则全站共用一个 key 就变成全局限流了。
别忽略客户端真实 IP 和代理转发问题
限流依据的“来源”如果直接取 r.RemoteAddr,在 Nginx 或云 WAF 后面会得到内网地址(如 10.0.1.5:32123),所有请求都算作同一个来源,彻底失效。
- 务必检查上游是否设置了
X-Forwarded-For或X-Real-IP,并在代码中解析(注意伪造头风险,需配置可信代理列表) - 用
net.ParseIP校验 IP 格式,拒绝非法值,防止缓存穿透或 key 泛滥(如传入X-Forwarded-For: 127.0.0.1,../../etc/passwd) - 如果业务允许,优先用用户身份标识(如 JWT 中的
sub字段)做限流 key,比 IP 更精准且不受网络拓扑影响
限流不是加个中间件就完事,key 的粒度、存储的范围、源头的可信度,三者缺一不可。线上出问题时,90% 是 key 设错了,而不是算法选错了。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











