滑动窗口限流需保留最近N个时间片计数并动态加权或截断,非简单清空旧桶;Go中用time.Now()删老数据实为固定窗口;应避免map并发读写panic,推荐预分配slice+原子操作实现轻量高效限流。

滑动窗口限流的核心是时间分片 + 计数叠加,不是简单清空旧桶
Go 里直接用 time.Now() 做“当前时间戳判断”来删老数据,本质是固定窗口,不是滑动窗口。真滑动窗口必须保留最近 N 个时间片的计数,并动态加权或截断——比如 60 秒窗口分 12 片(每片 5 秒),当前请求进来时,只剔除超过 60 秒的那几片,其余累加。
常见错误现象:panic: concurrent map read and map write,因为多个 goroutine 直接读写同一个 map[int64]int;或者用 sync.RWMutex 锁整个 map,吞吐骤降。
- 用
sync.Map或分片锁(shard lock)避免全局锁争用 - 时间片粒度别设太细(如 100ms),否则内存和计算开销陡增
- 不推荐用
time.Timer或time.Ticker清理过期桶——触发时机不可控,且清理本身可能延迟 - 建议用「懒清理」:每次请求进来时,先算出当前时间片索引,再顺手把早于
now.Add(-window)的桶置零或跳过
用 slice + 原子操作实现轻量滑动窗口(无依赖、低 GC)
比 map 更快、更省内存的方式是预分配一个固定长度的 []int64,每个元素代表一个时间片的请求数,用 atomic 做并发更新。关键在索引计算和过期判断不能出错。
使用场景:QPS 中等(
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 窗口总长
windowSec,分片数shards,则每片时长 =windowSec * 1e9 / shards(纳秒) - 当前片索引 =
(now.UnixNano() / shardDuration) % int64(shards),注意取模前转int64防溢出 - 过期检查不能只看索引差,得用绝对时间比对:某片时间戳 =
(index * shardDuration),对比now.UnixNano() - windowSec*1e9 - 示例中别直接
slice[i]++,必须用atomic.AddInt64(&slice[i], 1)
redis 实现分布式滑动窗口:用 ZSET 而非 INCR + EXPIRE
单机限流扛不住多实例,就得靠 redis。但很多人用 INCR key + EXPIRE key 模拟,这其实是固定窗口,且 EXPIRE 不保证准时失效,导致漏限或误限。
正确做法是用 ZSET 存请求时间戳为 score,member 可设为随机值或 clientID,再用 ZCOUNT + ZREMRANGEBYSCORE 组合。
-
ZADD rate:uid 1698765432.123 "req_abc"—— score 是 float64 时间戳(秒级或毫秒级需统一) -
ZCOUNT rate:uid (1698765372.123 +inf)统计过去 60 秒请求数(假设窗口 60s) - 紧接着
ZREMRANGEBYSCORE rate:uid -inf (1698765372.123)清理过期项,防止 zset 膨胀 - 注意:两次 redis 操作不是原子的,要用 Lua 脚本封装,否则中间插入请求会导致计数偏差
- 性能影响:ZSET 的
ZCOUNT是 O(log N),N 是该 key 下未过期成员数;高频写入时,定期ZCARD监控 size,超阈值可主动 trim
为什么不用 golang.org/x/time/rate?它不支持滑动窗口
rate.Limiter 底层是令牌桶,所有“速率”都是按固定周期(limit.Period)补充令牌,没有时间片切分和历史计数叠加能力。它的 AllowN 判断只看当前剩余令牌,无法回答“过去 30 秒内是否超了 100 次”这种问题。
容易踩的坑:rate.NewLimiter(100, 100) 看似是“100qps”,实际是“每秒最多补 100 个令牌,初始有 100 个”,如果瞬间来 200 请求,前 100 通过,后 100 被拒——但这 100 个被拒请求的时间分布完全不可控,无法做时段统计。
- 如果你需要的是“过去 N 秒累计请求数 ≤ M”,就别碰
rate.Limiter - 它适合平滑突发流量(如防雪崩),不适合合规类限流(如运营商要求“每分钟最多 50 次短信”)
- 想混用?可以组合:用滑动窗口做粗筛(时段计数),再用
rate.Limiter做细粒度速率控制,但要注意两层逻辑的窗口对齐
滑动窗口最难的不是实现,而是时间精度取舍和过期边界处理——纳秒 vs 毫秒、左闭右开 vs 左开右闭、是否允许跨片误差 1 片,这些细节一旦定错,压测时可能只在高并发下才暴露计数漂移。

















