滑动窗口限流必须用zset实现,因incr+expire仅支持固定窗口;zset通过zremrangebyscore清理过期时间戳、zadd插入新时间戳、zcard统计数量,三步原子执行。

为什么不用 redis.Incr 直接实现滑动窗口?
直接用 INCR + EXPIRE 只能做固定窗口,不是滑动窗口。滑动窗口要求「任意时间点往前看 N 秒内的请求总数」,而 Redis 原生命令不支持按时间范围聚合计数。硬拼多个 key(比如每秒一个 key)再 SCAN 或 MGET 求和,会放大网络往返、内存占用和 Lua 脚本复杂度,且在高并发下容易因 key 过多导致集群倾斜。
真正可行的路径只有一条:用 Redis 的有序集合(ZSET)存时间戳,靠 ZREMRANGEBYSCORE 清理过期成员,再用 ZCARD 获取当前窗口内数量。这是唯一兼顾原子性、精度和性能的方案。
滑动窗口限流的最小可行 Lua 脚本怎么写?
不要手写带事务或复杂逻辑的脚本,核心只需三步:清理旧数据、插入新时间戳、返回当前数量。下面这个脚本已在线上稳定跑半年,单次执行
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
<p>-- 删除窗口外的 timestamp
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)</p><p>-- 插入当前请求时间戳(用毫秒避免重复)
redis.call('ZADD', key, now, now)</p><p>-- 返回当前窗口内请求数
return redis.call('ZCARD', key)</p>
调用时传入:KEYS[1] 是限流 key(如 "rate:api:/login:192.168.1.1"),ARGV[1] 是当前毫秒时间戳,ARGV[2] 是窗口大小(单位:毫秒)。
- 必须用毫秒时间戳,否则同一秒内多次请求会被
ZADD覆盖(score 相同则更新 member) - 不要设 key 过期时间(
EXPIRE),由ZREMRANGEBYSCORE控制生命周期,避免 key 残留或提前消失 - 如果业务允许“最多 N 次/窗口”,脚本返回值 ≥ N 就该拒绝,别等
ZCARD后再判断
Golang 客户端怎么安全调用这个脚本?
用 github.com/go-redis/redis/v8 时,别把 Lua 脚本硬编码在 Eval 里,先用 Script.Load 加载一次,复用 *redis.Script 实例:
var slidingWindowScript = redis.NewScript(`
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
redis.call('ZADD', key, now, now)
return redis.call('ZCARD', key)
`)
<p>// 调用示例
result, err := slidingWindowScript.Run(ctx, rdb, []string{key}, time.Now().UnixMilli(), windowMs).Int64()</p>
注意几个坑:
-
time.Now().UnixMilli()必须在调用前获取,不能放在 Lua 里用redis.time()—— Redis 服务器时间和应用服务器时间可能不同步,误差超窗口大小就失效 - 如果 Redis 是集群模式,
key必须确保落在同一个 slot,建议用{rate:api:/login:192.168.1.1}这种带花括号的哈希标签 - 别忽略
err:redis.Nil表示 key 不存在(正常),其他错误(如连接断开、脚本语法错)要记录并降级(比如走本地令牌桶)
怎么压测验证滑动窗口行为是否正确?
写个简单测试:启动 10 个 goroutine,每个每 100ms 发一次请求,窗口设为 1000ms,限流阈值设为 8。观察第 9 次是否被拒,且 1s 后又能通过 —— 这说明窗口在滑动,不是固定切片。
更关键的是查 Redis 状态:
- 用
zrange key 0 -1 withscores看时间戳是否随请求递增、是否被自动清理 - 用
zcard key对比脚本返回值,确认一致性 - 故意把客户端时间调快 2s,看是否立刻触发大量拒绝 —— 这是校验时间戳依赖是否生效的最快方式
实际部署时,滑动窗口的精度完全取决于你传入的时间戳精度和网络延迟。如果应用层时钟漂移大,或者 Redis RT 波动超过 10ms,窗口边界就会模糊。这时候得考虑用客户端本地滑动窗口兜底,而不是强依赖 Redis 单点精度。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











