直接用time.Sleep做限流无效,因仅阻塞单goroutine;真正限流需共享状态:单机用rate.Limiter(令牌桶,支持突发),多实例必须Redis+Lua原子实现。

直接用 time.Sleep 做限流只会让服务更脆弱
它只阻塞当前 goroutine,HTTP 请求进来一个就起一个 goroutine,time.Sleep 完全不影响其他请求——你设了“每秒最多 10 次”,实际可能每秒放行几百次。这不是限流,是自我安慰。
真正要拦住流量,得有共享状态:要么单机内线程安全的计数器(如 rate.Limiter),要么跨进程/跨机器的原子存储(如 Redis)。别在 handler 里写 time.Sleep(100 * time.Millisecond),那是给压测留后门。
单机场景下,rate.Limiter 是唯一靠谱选择
golang.org/x/time/rate 提供的是令牌桶实现,不是玩具。它轻量、无锁路径优化好、支持突发控制,且 Wait() 和 TryConsume() 语义清晰。
-
rate.NewLimiter(rate.Every(time.Second/5), 3)表示“每 200ms 放 1 个令牌,桶最多存 3 个” → 等效于 5 QPS + 最多 3 次瞬时突刺 - 不要全局只用一个
rate.Limiter实例:所有接口共用一个桶,登录接口被刷崩,文档页也跟着 429 - 按
r.URL.Path或userID分 key 创建不同实例,用sync.Map缓存,避免每次新建 - 注意
burst不宜过大:设成 100 意味着短时间能攒 100 个请求,下游数据库可能直接被打满
多实例部署时,Redis + Lua 是唯一能落地的方案
K8s 部署 4 个 Pod,每个 Pod 自己维护一个 rate.Limiter,总容量就是单机阈值 × 4——你以为限 10 QPS,实际放行 40 QPS。
立即学习“go语言免费学习笔记(深入)”;
必须上 Redis,但绝不能拆成 INCR + EXPIRE 两步走:竞态下两个请求同时读到 0,都执行 INCR 并设过期,计数翻倍。
- 用 Lua 脚本保证原子性:
KEYS[1]是限流 key(如"user:123:api/v1/order"),ARGV[1]是阈值,ARGV[2]是窗口秒数 - 脚本里先
INCR,再判断是否为 1(即首次写入),是则EXPIRE;最后比对当前值是否超限 - Go 侧调用
redis.Eval(),返回整数结果:1 表示通过,0 表示拒绝 - 别把用户 ID 直接拼进 key 就完事——要防恶意构造超长 key 导致 Redis 内存暴涨,建议加哈希或截断
按用户/IP 限流时,key 设计决定成败
限流 key 不只是字符串拼接,它决定了维度精度、内存占用和攻击面。
- 纯 IP 限流(
"ip:192.168.1.100")容易被代理池绕过,适合粗粒度防护 - 带用户 ID 的 key(
"user:789:api/v1/pay")需确保用户已鉴权,未登录请求应走 IP 或设备指纹维度 - 路径级 key 必须规范化:去掉查询参数、统一大小写、过滤可变路径段(如
/user/123/profile→/user/:id/profile) - 避免用 session ID 或 JWT payload 做 key:前者不可靠,后者需解析且可能含敏感字段
key 设错比算法选错更致命——再准的滑动窗口,套在错误的维度上,也拦不住真实攻击。


















