滑动窗口限流需对齐窗口起始时间,正确做法是用 now.unixmilli() - now.unixmilli()%windowms 计算左边界;避免 truncate()、混用时间单位、滥用 sync.map;须原子化 tryacquire();用单调时钟防系统时钟干扰。

为什么 time.Now() 直接减毫秒会出错
滑动窗口的核心是按时间分桶,但很多人直接用 time.Now().UnixMilli() 除以窗口大小取整,结果发现限流不准——因为没对齐窗口起始时间。比如窗口 60 秒,从 12:00:00 开始,但你程序启动在 12:00:17,所有桶就偏移了 17 秒,历史数据无法复用,计数器永远“不完整”。
- 正确做法:用
now.UnixMilli() - now.UnixMilli()%windowMs算出当前窗口的左边界时间戳(即对齐到窗口起点) - 别用
time.Now().Truncate(),它返回的是time.Time,做 map key 或比较时容易因时区/精度引发隐式转换问题 - 如果窗口单位是秒,用
Unix();毫秒级必须用UnixMilli(),混用会导致桶数量暴增(比如误把 1000ms 当 1s,窗口被切碎成 1000 份)
用 sync.Map 还是 map + sync.RWMutex
高频写入场景下,sync.Map 并不比带读写锁的普通 map 快,尤其当 key 是时间戳这种强顺序、低重复率类型时——sync.Map 的内部分片和冗余拷贝反而拖慢写入。
- 滑动窗口 key 是时间戳(如
1717027200000),每次请求都可能生成新 key,sync.Map的 read map 命中率极低 - 推荐用
map[int64]int64+sync.RWMutex:读多写少时RLock几乎无开销,写操作只在窗口切换或计数超限时发生,锁粒度可控 - 别忘了定期清理过期桶,否则内存无限增长;建议在每次写入前,用
time.Now().UnixMilli() - windowMs算出截止时间,遍历删除小于该值的 key
GetCount() 和 TryAcquire() 怎么避免竞态
常见错误是先 GetCount() 判断是否超限,再 Inc(),中间存在竞态窗口——两个 goroutine 同时读到 99,都以为能通过,结果变成 101。
- 必须原子化:把“读旧值 → 判断 → 写新值”封装进一个带锁的函数,例如
TryAcquire()内部完成全部逻辑 - 不要暴露
GetCount()给外部调用者做判断依据;它只适合监控或 debug,不能用于控制流 - 如果需要支持动态限流阈值,把阈值也放进锁保护范围,避免读阈值和写计数之间出现不一致
时间精度丢失导致窗口跳变
本地测试时一切正常,部署到容器里却频繁触发限流——大概率是系统时钟被调整(NTP 同步、虚拟机休眠恢复),time.Now() 突然回跳或跳进,造成窗口计算错乱。
立即学习“go语言免费学习笔记(深入)”;
- 别依赖绝对时间戳做桶索引;改用单调时钟差值:用
time.Since(startTime)得到纳秒偏移,再换算成窗口编号 - 或者使用
runtime.nanotime()(非导出,但稳定)作为相对基准,避免系统时钟干扰 - 上线前务必压测时手动调快/调慢系统时间 5 秒,观察限流行为是否突变;这是最容易被忽略的生产隐患










