滑动窗口限流器不能直接用time.Tick实现,因其产生固定间隔tick,无法动态对齐窗口边界或按请求时间精确归属时间片;必须用绝对时间计算slot索引、预分配[]uint64+原子操作,并每次请求懒清理过期slot。

滑动窗口限流器为什么不能直接用 time.Tick 实现
因为 time.Tick 产生的是固定间隔的 tick,无法动态对齐窗口边界,也无法按请求时间精确归属到对应的时间片。滑动窗口的核心是“每个请求落在它发生时刻所属的窗口片段”,而不是“每秒清空一次计数器”。硬套 time.Tick 会导致窗口边界漂移、统计偏差,尤其在高并发或窗口粒度较细(如 100ms)时误差明显。
实际做法是:把时间轴切分成固定长度的 slot(比如 100ms 一个 slot),每个 slot 存一个计数器;请求到来时,计算它属于哪个 slot,更新该 slot 计数,并清理过期 slot。关键在于 slot 索引必须由绝对时间(如 time.Now().UnixMilli())推算,而非相对 tick。
- slot 长度越小,精度越高,但内存和清理开销越大
- 窗口总长 = slot 数 × slot 长度,例如 10 个 slot × 100ms = 1s 窗口
- 必须用原子操作或互斥锁保护 slot 计数器,避免竞态
用 sync.Map 还是 []uint64 存 slot 计数器
sync.Map 看起来灵活,但不适合滑动窗口场景——slot 是密集、有序、可预知索引的,用 map 带来哈希开销和 GC 压力,且无法保证遍历顺序,清理过期 slot 会变慢。更优解是预分配固定长度的 []uint64,配合原子操作。
示例结构:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type SlidingWindowLimiter struct {
slots []uint64
slotLenMS int64 // 单个 slot 毫秒数,如 100
totalSlots int // 总 slot 数,如 10
mu sync.RWMutex
baseTime atomic.Int64 // 最早有效 slot 的起始毫秒时间戳
}-
baseTime初始设为time.Now().UnixMilli()向下取整到 slot 边界 - 每次请求调用
getSlotIndex(now):(now - baseTime.Load()) / slotLenMS,结果可能为负或 ≥totalSlots - 负索引说明当前 slot 尚未创建,需先重置
baseTime并清零所有 slot;超界索引说明有旧 slot 过期,需原子递增baseTime并归零对应老 slot
如何安全地原子更新 slot 并清理过期数据
不能一边读 slot 一边遍历清理——清理逻辑本身也要并发安全。推荐做法:只在每次请求中做“最多一次”的清理,即检查最老 slot 是否已过期(baseTime + totalSlots×slotLenMS ≤ now),若过期则原子更新 baseTime,并用 atomic.StoreUint64(&limiter.slots[oldIdx], 0) 归零对应位置。
- 清理不是批量扫全数组,而是每次只处理一个待淘汰 slot,避免单次耗时抖动
- slot 数组本身不加锁,所有读写都走
atomic;仅baseTime更新和 slot 归零需原子操作 - 不要用
sync.Mutex包裹整个判断+更新流程,否则高并发下成为瓶颈 - 注意:Go 1.19+ 的
atomic.Int64支持CompareAndAdd,但这里只需Load和Store
怎么把限流器封装成 HTTP 中间件并避免 panic
直接在 http.HandlerFunc 里调用 Allow() 返回布尔值即可,但要注意两点:一是 Allow() 不应 panic,二是拒绝请求时要显式写响应,不能只 return。
典型中间件写法:
func WithRateLimit(next http.Handler, limiter *SlidingWindowLimiter) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}- 务必检查
Allow()返回值,不检查就等于没限流 - 不要在
Allow()内部打印日志或调用阻塞操作,它会被高频调用 - 如果需要按路径/用户维度区分限流,得在
Allow()入参传入 key,并维护多个独立限流器实例(如用sync.Map管理 key→limiter 映射),但注意 map 查找本身也需同步控制
滑动窗口真正的复杂点不在算法,而在于 slot 边界对齐、过期清理节奏、以及原子操作与锁边界的权衡——这些地方写错,限流效果就会时松时紧,甚至漏放或误拒。

















