golang.org/x/time/rate.limiter是首选,因其是官方轻量、线程安全、无锁令牌桶实现,单实例稳扛5k+ qps;误用new实例、混淆allow/wait语义或共用全局限流器会导致限流失效。

别用漏桶控 HTTP 接口,也别手写固定窗口计数器——它会在整秒切换时放行双倍流量,且无法修复。
为什么 golang.org/x/time/rate.Limiter 是首选
标准库没限流能力,rate.Limiter 是官方扩展包中唯一轻量、线程安全、无外部依赖的令牌桶实现。它用 atomic 操作而非互斥锁,单实例压测可稳扛 5k+ QPS;WaitN(ctx, n) 支持批量申请,比反复调 Allow() 减少锁竞争和 GC 压力。
常见误用包括:
- 每次请求
new rate.Limiter:burst 彻底失效,每个请求都从满桶开始 - 把
rate.Every(100*time.Millisecond)理解成“每 100ms 放行一个”,实际是“平均速率”,burst=3 就允许瞬间 3 个请求 - 在 handler 内部调
limiter.Wait():限流逻辑被路由分发绕过,部分接口完全不受控
Allow() 和 Wait() 到底该选哪个
Allow() 是纯判断:立刻返回 true 或 false,不阻塞、不等待,适合网关层快速拒绝;但它不保证后续 goroutine 不抢走同一枚令牌——高并发下多个请求同时 Allow() 都返回 true,结果瞬时超限。
Wait() 会阻塞直到拿到令牌或上下文取消,适合后端服务内部调用;但要注意:
- 频繁调
Wait()会创建大量time.Timer,GC 压力明显 - 前端已设 500ms 超时,而你等了 400ms 才失败,掩盖了真实瓶颈
- 必须用
WaitN(ctx, 1)并传入r.Context(),否则客户端断连后 goroutine 会泄漏
按 IP 或用户 ID 限流怎么写才不出错
不能直接在 middleware 里对所有请求共用一个 rate.Limiter 实例——那等于全站统一限流,登录接口和支付接口绑死在同一阈值上。
正确做法是用 sync.Map 动态维护分组限流器:
- 从
r.Header.Get("X-Real-IP")或c.ClientIP()提取真实 IP(注意 Nginx 需配proxy_set_header X-Real-IP $remote_addr;) - key 用
net.ParseIP(ip).To4()标准化,避免 IPv6 写法差异导致重复建桶 - value 存
*rate.Limiter,用LoadOrStore获取或新建,burst 设为 QPS 的 2–5 倍(如限 10 QPS,burst=30) - 不主动清理旧 key:靠
rate.Limiter自身的令牌衰减 + Go GC 回收,比定时遍历更轻量
滑动窗口为什么必须用 Redis + Lua
单纯用 Go 内存实现滑动窗口(比如环形数组存毫秒桶),在多实例部署下完全失效——每个进程只统计自己收到的请求。要跨节点一致,必须依赖共享存储。
Redis ZSET 是目前最可行方案,但关键点不在数据结构,而在执行逻辑:
- 不能拆成
ZADD+ZCOUNT+ZREMRANGEBYSCORE三步:并发时统计与清理不同步,必然漏判或误判 - 必须用 Lua 脚本原子执行:先清理过期成员,再统计当前窗口请求数,最后插入新时间戳
- score 必须用服务端毫秒时间戳(
redis.call("TIME")),不能用客户端传的时间,否则时钟漂移导致窗口错位 - member 必须全局唯一(如
ip:timestamp:rand),否则相同时间戳的多个请求会覆盖
真正难的不是写脚本,而是验证它在 1000+ QPS、3 节点 Redis 集群、时钟偏差 ±200ms 下仍不丢不重——这需要压测时注入故障,不是跑通 demo 就算完。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











