限流策略必须与鉴权解耦,通过独立中间件实现:先鉴权(返回401/403),再限流(返回429并带标准限流头);限流key须基于鉴权后的可信上下文构造并归一化;burst值应≤后端单实例平均QPS的2倍,关键接口禁用burst;Redis限流须用原子Lua脚本确保EXPIRE与计数更新一体执行。

限流策略必须和鉴权解耦,否则会污染权限逻辑
鉴权模块只该回答“能不能过”,限流是独立的流量调控问题。把 rate.Limiter 或自定义限流器硬塞进鉴权函数里,会导致权限校验逻辑被速率判断干扰,比如 Unauthorized 错误实际是被限流触发的,日志和监控就全乱了。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用中间件链式调用:先走鉴权中间件(检查 token、role、scope),再走限流中间件(基于 path、user_id 或 client_ip)
- 限流中间件应返回标准 HTTP 状态码:
429 Too Many Requests,且响应头带上X-RateLimit-Limit和X-RateLimit-Remaining - 避免在鉴权函数里调用
limiter.Allow();鉴权成功后,由下一个中间件决定是否放行
用 token bucket 实现 per-user 动态限流时要注意 key 构造
固定速率限流(如每秒 100 次)简单,但按用户维度限流容易出错——user_id 来自 JWT payload,但未校验签名就直接取值,可能被伪造;或没做标准化(大小写、前缀空格、匿名用户用 "anonymous" 还是 "")。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 限流 key 必须基于可信上下文构造:鉴权完成后从
ctx.Value("user_id")取,而不是从原始请求 header 或 query 中解析 - 对 key 做归一化处理:
strings.TrimSpace(strings.ToLower(id)),并显式排除空值(返回默认限流桶或拒绝) - 不要用 map 存桶实例:高并发下
sync.Map虽线程安全,但 GC 压力大;改用golang.org/x/time/rate.Limiter配合 LRU cache(如github.com/hashicorp/golang-lru)缓存活跃用户桶
burst 参数设太高会导致突发流量打穿后端
rate.NewLimiter(10, 5) 表示“每秒 10 次,最多允许 5 次突发”。看似合理,但如果用户在 100ms 内发 5 次请求,全部通过,紧接着后端服务刚好在扩容窗口期,就可能雪崩。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- burst 值不应超过后端单实例平均 QPS 的 2 倍(例如后端单机扛 200 QPS,burst ≤ 400)
- 对关键接口(如支付回调、库存扣减)禁用 burst:
rate.NewLimiter(1, 1),强制匀速 - 用
limiter.ReserveN()替代AllowN(),可提前预判等待时间,配合超时控制(如等待 > 200ms 直接拒绝)
Redis 分布式限流要防原子性失效
用 Lua 脚本实现令牌桶时,常见错误是把 INCR 和 EXPIRE 拆成两条命令——网络中断或 Redis 主从切换时,key 可能没设置过期时间,导致永久封禁。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须用单个 Lua 脚本完成“读当前值 → 判断是否超限 → 更新计数 → 设置过期”四步,例如:
redis.Eval(ctx, luaScript, []string{key}, nowUnix, windowSec, maxCount) - 脚本内用
redis.call("EXPIRE", KEYS[1], ARGV[2])而不是redis.call("PEXPIRE", ...),避免毫秒级精度引发的边界问题 - 本地 fallback 不等于关闭限流:当 Redis 不可用时,降级为内存
rate.Limiter(带告警),而非直接放行
真正难的不是写一个限流器,而是让不同策略(用户级、IP 级、接口级)共存时不互相覆盖,且每个策略的配额变更能热生效——这需要配置中心 + 版本化规则引擎,不是靠加个 mutex 就能搞定的。


















