滑动窗口限流必须用zset+lua脚本实现,因incr+expire仅为固定窗口,存在临界流量翻倍、并发不一致、时间差错乱等问题;zset需配合lua原子执行清理、统计、写入,并用服务端时间戳、毫秒score、唯一member及严格参数顺序确保正确性。

Go语言里做接口限流,别一上来就堆 Sentinel 或写分布式 Redis 脚本——先看清楚你真正要解决的问题:是防爬虫、扛秒杀、保下游,还是单纯避免自己 CPU 爆掉。不同算法在 rate.Limit、capacity、time.Now() 的处理逻辑上差异极大,选错会导致“限了等于没限”或者“用户点一下卡三秒”。
用 golang.org/x/time/rate 实现令牌桶时,Allow() 和 Wait() 的行为区别
Allow() 是非阻塞判断:立刻返回 true 或 false,适合对延迟敏感的场景(比如 API 网关快速拒绝);Wait() 会阻塞直到拿到令牌或超时,适合后端服务内部调用,配合 context.WithTimeout 控制等待上限。
-
Allow()不做任何等待,但要注意它不保证后续请求能立即通过——因为桶里可能只剩 1 个令牌,而并发请求同时调用Allow()可能都拿到true,导致瞬时超限 -
Wait(ctx)内部用了time.Timer,频繁调用会创建大量定时器,高 QPS 下 GC 压力明显;生产环境建议用WaitN(ctx, n)批量申请,减少锁竞争 - 如果业务允许排队,
Wait()比手动 sleep + 重试更可靠;但如果前端已设 500ms 超时,Wait(ctx)等 200ms 后再失败,反而掩盖了真实瓶颈
固定窗口计数器为什么会在日志里看到“0.99 秒突增 2 倍流量”
固定窗口的本质是“每到整点就清零”,比如每秒限 100 次,那 [13:00:00.000, 13:00:00.999] 和 [13:00:01.000, 13:00:01.999] 是两个独立窗口。攻击者只要在窗口切换前 10ms 发起 100 请求,再在下一窗口开头再发 100,20ms 内就打进了 200 次。
- 这个问题不是 Go 实现缺陷,而是算法模型固有边界——所有基于“重置时间戳”的计数器都存在
- 如果你用
sync.Map+time.Now().UnixSecond()自己实现,同样会踩坑;必须改用滑动窗口(如环形数组存最近 N 个毫秒桶)或令牌桶才能缓解 - 临时补救:把窗口从 1 秒改成 500ms,能减半临界穿透概率,但监控指标会变碎,聚合成本上升
漏桶算法在 Go 里不适合直接用于 HTTP 接口限流
漏桶输出速率恒定,比如设置“每秒漏 10 个请求”,那无论上游是匀速来还是瞬间压进 100 个,下游永远只能按 10 QPS 处理。这对带宽整形或上传限速合理,但对 Web 接口就是反模式——用户点击提交按钮,等 10 秒才响应,体验直接崩坏。
-
juju/ratelimit的NewBucket是典型漏桶,它的Take()方法会阻塞到漏出足够令牌,不适合交互式接口 - 如果你真要用漏桶逻辑,至少得配合缓冲队列(如
chan *http.Request)+ 工作 goroutine 消费,否则阻塞会卡死整个 HTTP server - 真正需要漏桶的场景极少:比如 SaaS 平台给客户分配固定 API 调用带宽,且明确要求“不能借下个月额度”,这时才值得引入
分布式限流为什么不能只靠 redis.Incr() + Expire()
单次 INCR 和 EXPIRE 不是原子操作,中间若进程崩溃,key 会永久存在且无过期时间,导致后续所有请求被误限。必须用 Lua 脚本封装,确保“计数+设过期”一步完成。
- 常见错误:先
INCR key,再EXPIRE key 60—— 若第二步失败,key 成为永不过期的僵尸计数器 - 正确写法是 Lua 脚本里用
redis.call('INCR', KEYS[1])+redis.call('PEXPIRE', KEYS[1], ARGV[1]),并用redis.Eval()一次性执行 - Redis 集群环境下,key 必须带
{}标签确保路由到同一 slot,否则EVAL可能跨节点失败
真正难的不是选算法,而是确认你的“限流目标”是否可测量:如果监控里连真实 QPS 都没对齐,再准的令牌桶也救不了架构设计缺陷。先接好 prometheus.ClientGatherer,让限流器上报 rate_limit_exceeded_total 和 rate_limit_wait_seconds 直方图,再谈优化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











