go 用 golang.org/x/time/rate 实现突发流量友好型限流,核心是令牌桶模型,通过 rate.every 控制生成节奏、burst 设置桶容量,支持 allow() 非阻塞判断与 reserven() 预占令牌;需按 ip 或路径用 sync.map 动态管理限流器并加过期清理;分布式场景才需 redis,且应权衡计数器与真正令牌桶精度;waitn/allown 选择影响用户体验,429 响应须带限流头。

用 golang.org/x/time/rate 实现突发流量友好型限流
直接用 Go 官方 rate.Limiter 是最轻量、最可靠的选择,尤其适合应对突发流量——它本质就是令牌桶,支持「桶里攒着的令牌一次性用掉」,不像漏桶那样死卡速率。关键不是“能不能限”,而是“怎么让桶既能扛住短时洪峰,又不放水过头”。
核心参数只有两个:rate.Every 控制令牌生成节奏,Burst 决定桶容量。比如 rate.NewLimiter(rate.Every(time.Second/10), 20) 表示:每 100ms 放 1 个令牌(即长期稳在 10 QPS),但桶最多存 20 个,意味着 2 秒内允许最多 20 次请求打进来——这正是秒杀或热点事件需要的弹性。
-
Allow()是非阻塞判断,适合「超了就拒」场景;ReserveN()可预占 N 个令牌并返回等待时间,适合需排队降级的逻辑 - 不要把同一个
*rate.Limiter实例复用于不同 IP 或路径——否则 A 用户刷爆了,B 用户也跟着被拦 - 桶容量
Burst建议设为长期 QPS 的 2–3 倍,太小扛不住突增,太大等于没限
按 IP 或路径做细粒度限流必须用 sync.Map 缓存实例
全局一个限流器只能控总量,真实业务要的是「每个用户每分钟最多 60 次」或「/api/pay 接口每秒最多 5 次」。这时必须为每个维度动态创建并复用 *rate.Limiter,而不能每次请求都 new 一个——否则内存暴涨且 GC 压力大。
推荐用 sync.Map 存储,key 是 c.ClientIP() 或 c.Request.URL.Path,value 是对应限流器。注意两点:
- 初始化时传入合理的
Burst,比如按 IP 限流可设为 60,按敏感接口限流可设为 5–10 - 必须加过期清理机制,例如用
time.AfterFunc在 limiter 空闲 5 分钟后从 map 中删掉,否则长连接多的系统内存会持续上涨 - 别在
Allow()前加额外锁——rate.Limiter本身并发安全,加锁反而拖慢性能
Redis 分布式限流只在多实例部署时才真正必要
单机部署用 rate.Limiter 足够,一旦上了 K8s 或多台机器负载均衡,本地内存里的限流状态就不同步了。这时才需要 Redis,但要注意:不是所有场景都值得上。
gin-vue-admin 用的 limit_ip.go 是计数器模型(非令牌桶),靠 INCR + EXPIRE 实现,简单但有边界问题:比如窗口最后一秒猛刷 100 次,下一秒初又刷 100 次,实际是 200 次/秒,却没触发限制。而真正令牌桶的 Redis 实现(如 ulule/limiter)需要 Lua 脚本保证原子性,复杂度和延迟都更高。
- 如果只是防爬虫或普通 API 限流,计数器够用;对精度要求高(如支付类接口),必须上真正的令牌桶 Redis 实现
-
ulule/limiter的redisstore默认 key 带前缀rate_limiter:,上线前确认是否和已有 Redis key 冲突 - Redis 网络 RTT 会增加约 0.5–2ms 延迟,QPS 过万时建议压测对比本地限流与 Redis 限流的实际吞吐差异
AllowN 和 WaitN 的选择决定用户体验底线
限流不是只为了拒绝请求,更是为了控制失败方式。同一限流器下,AllowN(1) 立刻返回 true/false,适合前端能友好提示的场景;WaitN(1, 500*time.Millisecond) 会最多等 500ms,适合后端服务间调用——你希望对方稍等一下而不是立刻失败。
-
WaitN阻塞期间仍占用 goroutine,高并发下慎用;ReserveN返回Reservation对象,可调Delay()判断要不要等,更灵活 - 别在 HTTP handler 里无条件调
WaitN,必须设 max wait 时间,否则恶意客户端可构造长等待耗尽你的 goroutine - 返回
429 Too Many Requests时,务必带X-RateLimit-Remaining和X-RateLimit-Reset头,方便前端重试调度
rate.NewLimiter,而是想清楚:这个接口到底能容忍多少突发?它的失败对上下游意味着什么?桶空了之后,是该静默丢弃、排队等待,还是降级返回缓存?这些决策比代码本身影响更大。











