滑动窗口限流不能直接用 gin-contrib/limiter,因其仅支持固定窗口,存在临界点流量突刺问题;而滑动窗口需基于 redis zset 实现时间排序、原子清理与统计,才能真实反映“最近 n 秒”请求频次并保障分布式一致性。

滑动窗口限流为什么不能直接用 gin-contrib/limiter
因为 gin-contrib/limiter 默认只支持固定窗口(fixed window),比如“每分钟最多 100 次”,但窗口切换瞬间会出现流量突刺——前一秒末 + 后一秒初可能各来 100 次,实际 1 秒内就扛了 200 次。滑动窗口能平滑统计,比如“最近 60 秒内最多 100 次”,更贴近真实负载压力。而该库不暴露底层计数器操作逻辑,也没提供滑动窗口的存储结构接口,硬套会绕弯甚至失效。
用 Redis + ZSet 实现滑动窗口的核心逻辑
滑动窗口本质是维护一个按时间排序的请求记录集合,并剔除过期项。Redis 的 ZSet 天然适合:score 存时间戳(秒级或毫秒级),member 存唯一标识(如 ip:uri 或 token:uri),再用 ZCount 和 ZRemRangeByScore 配合完成统计与清理。
- 每次请求先
ZRemRangeByScore清掉早于now - window_sec的所有 entry - 再
ZCount查当前窗口内数量是否超限 - 没超则
ZAdd当前请求(score = now,member =ip:uri),并设EX过期保障内存安全 - 注意:
ZCount的 range 是左闭右闭,所以用now - window_sec + 1作为 min score 更稳妥(避免浮点或时钟抖动导致漏删)
Gin 中间件里怎么安全集成 ZSet 限流
关键不是“加个中间件”,而是避免阻塞、并发冲突和 Redis 连接泄漏。不要在中间件里新建 redis.Client,必须复用全局连接池;所有 Redis 操作要用 pipeline 合并,减少 RTT;错误要区分 redis.Nil(正常)和网络错误(需重试或降级)。
func SlidingWindowLimiter(redisClient *redis.Client, windowSec int, maxReq int) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
uri := c.Request.URL.Path
key := fmt.Sprintf("sliding:%s:%s", ip, uri)
now := time.Now().Unix()
<pre class="brush:php;toolbar:false;"> pipeline := redisClient.TxPipeline()
pipeline.ZRemRangeByScore(key, 0, now-int64(windowSec))
pipeline.ZCount(key, now-int64(windowSec), now)
pipeline.ZAdd(key, &redis.Z{Score: float64(now), Member: ip})
pipeline.Expire(key, time.Duration(windowSec+10)*time.Second) // 预留缓冲
_, err := pipeline.Exec(c.Request.Context())
if err != nil && err != redis.Nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "rate limit service unavailable"})
return
}
cnt, _ := pipeline[1].(*redis.IntCmd).Result() // ZCount 结果在第二个命令
if int(cnt) >= maxReq {
c.AbortWithStatus(http.StatusTooManyRequests)
return
}
}}
为什么不用本地内存(如 sync.Map)做滑动窗口
单机 sync.Map 做滑动窗口看似简单,但有三个硬伤:
- 多实例部署时完全不共享状态,限流失去意义
- 无法自动清理过期条目,ZSet 的
ZRemRangeByScore是原子且高效的操作,而自己遍历sync.Map删除会锁整个 map 或引入复杂分段逻辑 - 高频请求下,每秒几百次写 + 扫描,CPU 和 GC 压力远高于一次 Redis pipeline(实测 QPS > 500 后本地方案延迟抖动明显)
真正需要纯内存场景,建议改用 golang.org/x/time/rate 的 Limiter(令牌桶),它轻量、无依赖,但不是滑动窗口——别强行魔改它去模拟滑动窗口,精度和边界条件很难对齐。











