滑动窗口在分布式系统中必须用Redis而非本地内存,因为本地计数器(如sync.Map或atomic.Int64)仅限单机,多实例会各自维护独立计数,彻底丧失全局限流意义;Redis凭借原子性与全局可见性成为硬性前提,且必须用Lua脚本或Redis 6.2+的INCRBYEX等原子命令,禁用非原子的INCR+EXPIRE组合,否则窗口内计数可能超发。

滑动窗口为什么必须用 Redis 而不是本地内存
本地计数器(比如 sync.Map 或 atomic.Int64)在单机场景下能跑通,但分布式系统里多个 Go 实例会各自维护一套计数,完全失去限流意义。Redis 的原子性 + 全局可见性是硬性前提——哪怕你只部署两台服务,也得切到 Redis。
注意:不要用 INCR + EXPIRE 分两步操作,这是经典竞态漏洞。必须用 Lua 脚本或 Redis 6.2+ 的 INCRBYEX 原子命令,否则窗口内计数可能超发。
用 INCRBYEX 实现滑动窗口的最小可行代码
Go 客户端推荐 github.com/redis/go-redis/v9,它原生支持 INCRBYEX(Redis >= 6.2)。关键不是“怎么写”,而是参数含义容易错:
-
key必须带用户/接口维度标识,例如"rate:uid:123:api:/order/create",不能只用"rate:api" -
increment固定填1,别传变量 -
expiry是窗口总时长(秒),不是“剩余过期时间”;比如 1 分钟窗口就传60,Redis 会自动按首次写入时间对齐 - 返回值是本次递增后的累计值,需立刻跟阈值比对,不能先
GET再判断
示例片段:
立即学习“go语言免费学习笔记(深入)”;
ctx := context.Background()
key := "rate:uid:" + uid + ":api:" + path
val, err := rdb.IncrByEX(ctx, key, 1, 60).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return false, err
}
return val <= int64(limit), nil
时间精度陷阱:为什么 1 秒窗口实际会漂移
Redis 的 INCRBYEX 按 key 首次写入时间作为窗口起点,后续所有请求都基于这个时间戳计算是否过期。这意味着:
- 第一个请求在 12:00:00.123 到达 → 窗口是 [12:00:00.123, 12:01:00.123)
- 第二个请求在 12:00:00.888 到达 → 还在同一个窗口内,但你预期的“整秒对齐”并不存在
如果业务强依赖整秒/整分钟对齐(比如监控报表),得放弃 INCRBYEX,改用 Lua 脚本手动分片:把 60 秒拆成 60 个 1 秒 key,用 ZSET 或 HASH 存各秒计数,再 ZREMRANGEBYSCORE 清过期段。复杂度陡增,非必要不碰。
漏桶 vs 滑动窗口:Go 服务里该选哪个
滑动窗口响应快、实现轻,但内存/Redis 压力随并发 key 数线性上涨;漏桶平滑但需要维护后台 goroutine 定期滴答,且 Redis 里模拟漏桶得用 LIST + LLEN + LTRIM,延迟更高。
直接结论:
- API 网关层限流(key 维度少,如 /login、/pay)→ 无脑滑动窗口 +
INCRBYEX - 用户级精细限流(百万 uid)→ 改用令牌桶,客户端预取 token,服务端只做
DECR校验 - 要精确控制请求间隔(如“每 5 秒最多 1 次”)→ 漏桶不可替代,但得接受 100ms 级延迟波动
真正卡住多数人的不是算法选择,而是没意识到:Redis 往往成了限流链路的单点瓶颈。压测时 INCRBYEX QPS 超 5w 就得考虑分片 key 或本地缓存 fallback 逻辑。


















