滑动窗口限流不能只用INCR+EXPIRE,因其只能实现固定窗口,无法精确统计“当前时间往前T秒”的动态请求量;必须用ZSET配合Lua脚本原子实现:先清理过期score、再ZADD新请求、最后ZCOUNT统计,同时需注意键设计、内存清理与水位监控。

滑动窗口限流为什么不能只用 INCR + EXPIRE
因为单个 key 无法表达“过去 N 秒内”的动态时间切片。用 INCR 配合固定过期时间,实际是固定窗口(如每分钟重置),会放行突增流量(比如第 59 秒进 100 个请求,第 0 秒又进 100 个,实际 2 秒内 200 次)。滑动窗口必须能按毫秒级精度统计「当前时间往前推 T 秒」的所有请求。
Redis 原生不提供滑动窗口原子计数,得靠有序集合(ZSET):把请求时间戳作为 score,唯一 ID(如 req:{uuid})作为 member,再用 ZCOUNT 查区间,ZREMRANGEBYSCORE 清理过期项。
Go 中用 redis.ZAdd 和 ZCount 实现原子滑动检查
关键不是“加一次再查一次”,而是把写入和统计合并成一个 Lua 脚本——否则并发时可能写入成功但统计漏掉刚插入的条目。
- 脚本接收参数:
key(限流器标识)、now(毫秒时间戳)、window_ms(窗口长度,如 60000)、max_count(最大请求数) - 先
ZREMRANGEBYSCORE key 0 (now - window_ms)清理过期请求 - 再
ZADD key now req_id插入新请求(score 是当前时间) - 最后
ZCOUNT key (now - window_ms) +inf获取当前窗口请求数 - 返回是否 ≤
max_count,以及当前计数值(便于调试)
Go 侧调用示例:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
script := redis.NewScript(`
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local max_count = tonumber(ARGV[3])
<p>redis.call("ZREMRANGEBYSCORE", key, 0, now - window_ms)
redis.call("ZADD", key, now, ARGV[4])
local count = redis.call("ZCOUNT", key, now - window_ms, "+inf")
return {count <= max_count, count}
`)</p><p>result, err := script.Run(ctx, rdb, []string{"rate:login:192.168.1.100"}, time.Now().UnixMilli(), 60000, 100, "req:"+uuid.NewString()).Result()
if err != nil {
// handle
}
allowed := result.([]interface{})[0].(int64) == 1
current := result.([]interface{})[1].(int64)
注意 ZSET 内存膨胀和清理时机
每个请求都塞一个 member,高频服务一天可能上亿条,不清理会撑爆 Redis 内存。
- 脚本里
ZREMRANGEBYSCORE是必须的,且要放在ZADD前——避免刚插入就被删,导致计数偏少 - 不要依赖 TTL 自动过期:
ZSET本身不支持 key 级 TTL 与成员生命周期联动;必须显式清理 - 如果业务允许轻微不准,可改为“惰性清理”:只在每次检查时删过期项,不额外起 goroutine 扫描
- 更激进的做法是加
EXPIRE给 key 设个较长 TTL(如 2 小时),配合脚本内清理,双重保险
限流键设计要带业务上下文,别只用 IP
单纯用 rate:ip:{ip} 容易被共享 IP(NAT、代理)误伤,也防不住单用户多设备刷量。
- 优先组合维度:
rate:uid:{user_id}、rate:token:{hashed_token}、rate:api:{path}:{method} - 如果必须用 IP,至少加上 User-Agent 哈希或 TLS fingerprint 做二级区分(注意隐私合规)
- 键名里避免特殊字符,用冒号分隔,方便 Redis 的
KEYS rate:*调试(但生产禁用KEYS) - 高并发下键名太长会增加网络开销,控制总长度在 128 字节内较稳妥
滑动窗口真正的复杂点不在脚本怎么写,而在键粒度选择和内存水位监控——没配好 maxmemory 和淘汰策略,Redis OOM 时整个限流就失效了。

















