单机限流用 rate.limiter,集群需 redis+lua 全局限流;参数含义为平均速率与突发容量,非简单qps上限;http中间件应非阻塞判断并立即返回429;redis限流必须用lua保证原子性;迁移时需区分网关/业务层限流、暴露监控指标、配置本地缓存兜底及动态阈值能力。

单机限流用 golang.org/x/time/rate 就够了,但微服务集群必须用 Redis + Lua 实现全局限流;语言学习要先吃透 rate.Limiter 的参数语义,再过渡到分布式场景的原子性约束。
为什么 rate.NewLimiter(10, 50) 的两个参数不能随便调
第一个参数是平均速率(每秒令牌数),第二个是突发容量(桶大小)。它不是“最多每秒处理10个请求”,而是“允许短时爆发最多50个请求,但长期平均不能超过每秒10个”。
- 设成
rate.NewLimiter(100, 10):桶太小,哪怕流量均匀也会频繁触发限流,因为每来一个请求都要等令牌,实际吞吐远低于100 - 设成
rate.NewLimiter(1, 100):桶大但补速慢,适合防爬虫——允许一次性突袭100次,之后几乎卡死 - HTTP 中间件里每次调用
limiter.Allow()是非阻塞的,返回false就该立刻返回http.StatusTooManyRequests,别 sleep 等令牌
Redis 固定窗口限流的 Lua 脚本必须自己写,别信封装库
很多 Go 限流库用 INCR + EXPIRE 两步操作,但在高并发下可能丢失计数:EXPIRE 失败后 key 永久存在,导致后续永远限流。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法是用 Lua 脚本保证原子性,例如:
local key = KEYS[1] local window = tonumber(ARGV[1]) local limit = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local window_start = now - window local count = redis.call('ZCOUNT', key, window_start, '+inf') if count >= limit then return 0 end redis.call('ZADD', key, now, math.random()) redis.call('EXPIRE', key, window + 1) return 1 - key 要包含业务标识,比如
"rate:uid:12345:/api/order",避免不同用户或接口互相干扰 - 注意 Redis 连接池配置:限流是高频操作,
MaxIdle和MaxActive至少设为 20+,否则连接等待拖慢整个请求链路
从单体限流迁移到微服务限流时,最容易漏掉的三件事
不是换掉 rate.Limiter 就算完成迁移,关键在边界和可观测性。
- 网关层限流和业务层限流要分清楚:API 网关做粗粒度(按 IP 或 token),服务内部做细粒度(按 user_id 或 order_id),两者阈值不能冲突
- 没暴露限流指标:Prometheus 中至少要上报
rate_limit_total{service="order", result="blocked"}和rate_limit_current{key="uid:123"},否则无法定位谁被限、为什么被限 - 本地缓存兜底缺失:Redis 不可用时,直接 fallback 到
rate.Limiter会误放大量请求;应加一层内存缓存(如sync.Map)记录最近 1 分钟的拒绝数,超阈值则强制开启本地限流
真正难的不是写对 Lua 脚本,而是把限流阈值变成可配置、可灰度、可回滚的运行时能力——比如通过 etcd 动态下发 /ratelimit/user/12345 的值,而不是硬编码在代码里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










