Redis ZSET 是滑动窗口限流唯一可靠解法;固定窗口可用 INCR+EXPIRE+Lua,禁用 get+set+expire 非原子组合,否则导致漏计数、过期覆盖或 key 永不删除。

直接结论:用 Redis ZSET 实现滑动窗口限流,是当前生产环境最稳妥的选择;若业务允许短时突增(如后台管理接口),可用 INCR + EXPIRE 固定窗口,但必须用 Lua 脚本保证原子性。
为什么不能用 get + set + expire 组合
这是新手最容易踩的坑——高并发下出现漏计数、过期时间被反复覆盖、甚至 key 永不删除。典型错误逻辑:
- 先
redisTemplate.opsForValue().get(key)判断是否存在 - 再
redisTemplate.opsForValue().set(key, "1"),然后redisTemplate.expire(key, 60, SECONDS)
问题在于:两次 Redis 命令之间存在竞态。A 请求刚读完为 null,B 请求也读完为 null,两者都执行 set + expire,结果只有一条过期时间生效,且计数从 1 开始而非累加。
正确做法:所有写操作必须原子化。Spring Boot 中推荐封装 Lua 脚本,或直接使用 redisTemplate.execute(new DefaultRedisScript<>()) 执行一段带 KEYS/ARGV 的脚本。
固定窗口限流:INCR + EXPIRE 必须配 Lua
适用于低敏感度场景(如导出按钮、配置查询),核心是“每 N 秒最多 M 次”,不追求任意连续 N 秒的严格控制。
关键点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
key设计要唯一:建议格式为"rate:ip:" + ip + ":uri:" + requestUri,避免不同路径或 IP 冲突 - 过期时间单位必须和业务单位一致:配置项写的是
60,代码里就得传TimeUnit.SECONDS,别误传成MILLISECONDS - Lua 脚本示例(必须):
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_sec = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, expire_sec)
end
if current > limit then
return 0
end
return 1调用时传入 key、limit、expire_sec 三个参数,返回 1 表示放行,0 表示拒绝。
滑动窗口限流:ZSET 是唯一可靠解法
解决“00:59 和 01:01 各来 100 次”这类边界突刺问题,本质是统计“当前时刻往前推 N 秒内”的请求数。
技术要点:
- 每次请求存入
ZADD key timestamp request_id,timestamp用System.currentTimeMillis() - 清理过期项必须前置:
ZREMRANGEBYSCORE key -inf (now - window_ms),否则 zset 会无限膨胀 - 统计数量用
ZCOUNT key (now - window_ms) +inf,注意区间是左开右闭 - Spring 中不要依赖
opsForZSet().rangeByScore(),它不自动清理,得手动补zRemRangeByScore()
key 示例:"sliding:ip:" + ip + ":uri:" + requestUri;window_ms 若为 60 秒,就用 System.currentTimeMillis() - 60_000 计算左边界。
@RateLimiter 注解 + AOP 的封装要点
把限流逻辑从 Controller 剥离,靠注解驱动,但要注意几个隐蔽陷阱:
- 注解属性别写死 IP,应从
HttpServletRequest动态提取,否则多实例部署时失效 - 拦截器或 AOP 切面中,务必在
try...finally块里执行限流检查,防止异常绕过校验 - 不要在切面里直接 new RedisCallback,而应复用
RedisTemplate的线程安全实例 - 如果用了
@IpLimiter(limit = 100, time = 60)这类注解,背后实现必须区分固定窗口 or 滑动窗口,不能默认混用
真正难的不是写出来,而是想清楚:你的业务能否容忍两秒内翻倍的流量?能,就用固定窗口 + Lua;不能,就必须上 ZSET,且必须每轮都做 ZREMRANGEBYSCORE 清理——这点最容易被忽略,上线后内存涨得悄无声息。

















