滑动窗口限流比固定窗口更可靠,因其以当前时刻为右边界动态维护时间窗口(如最近1秒),用redis zset存储时间戳并配合zremrangebyscore清理过期数据、zcard统计实时请求数,彻底解决窗口边界突刺问题。

直接结论:用 Redis + 滑动窗口(zset)比固定窗口更可靠,尤其在高并发、时间边界敏感的场景下;但若业务允许宽松限流,固定窗口(incr + expire)实现更快、调试更直观。
很多团队一开始用 redisTemplate.opsForValue().increment() 配合 expire() 做“每分钟最多 100 次”,上线后发现凌晨 00:59 和 01:01 各来 100 次,实际两秒内就扛了 200 次——这不是 bug,是固定窗口天然缺陷。下面说清楚怎么选、怎么写、怎么避坑。
固定窗口限流:适合简单场景,但必须注意并发安全
适用于内部管理接口、低频调用、或对“短时峰值”不敏感的业务(如后台导出按钮)。核心是:INCR + EXPIRE 原子性保障。
- 错误写法:先
get()再set()+expire()—— 高并发下会漏计数或重复设过期时间 - 正确做法:用 Lua 脚本封装原子操作(Spring Boot 中推荐
redisTemplate.execute(new DefaultRedisScript(...))) - key 设计建议用
"rate:ip:" + ip + ":url:" + requestUri,避免不同用户/路径冲突 - 注意
expireTime单位要和业务单位一致(比如配置 60 表示秒,代码里别误当成毫秒传给expire())
滑动窗口限流:用 zset 实现任意时间窗口内精确计数
真正解决“00:59 到 01:01 突增”问题。本质是把每次请求时间戳作为 score 存进 ZSET,用 ZCOUNT 查指定时间范围内的 member 数量。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- key 示例:
"sliding:ip:" + ip + ":url:" + requestUri - 插入命令:
ZADD key timestamp request_id(timestamp 用System.currentTimeMillis()) - 查询命令:
ZCOUNT key (start_timestamp +inf),其中start_timestamp = now - window_ms - 务必搭配
ZREMRANGEBYSCORE清理过期项,否则 zset 无限膨胀(可在每次检查前执行) - Spring 中推荐封装成 RedisCallback 或用
redisTemplate.opsForZSet().rangeByScore(),但注意它不自动清理,得自己加zremrangebyscore
@RateLimiter 注解 + AOP:让限流逻辑和业务彻底解耦
不是所有接口都需要限流,也不是所有限流规则都一样。用注解驱动最灵活,但要注意切点表达式和参数提取逻辑。
- 注解字段建议至少包含:
count(阈值)、time(窗口秒数)、keyType(枚举:IP / USER_ID / URL / COMBINED) - AOP 中 key 生成不能只拼
joinPoint.getSignature().toShortString(),要结合上下文(如从 JWT 解析userId,或从 header 取X-Real-IP) - 拦截器里不要 throw 异常再全局捕获——直接
response.setStatus(429)+response.getWriter().write("too many requests")更轻量 - 如果用了 Spring Security,注意
RequestContextHolder在异步线程中为空,需手动传递请求上下文
生产环境必须验证的三个细节
限流不是加完就完事。这三个点不验证,上线等于裸奔:
- Redis 连接池配置是否合理?默认
max-active=8在压测时极易成为瓶颈,建议设为 32–64,并开启test-on-borrow - 限流失败时是否记录日志?至少要打
WARN级别,含key、currentCount、limit、ip、uri,方便溯源攻击源 - 是否考虑多实例部署下的 key 冲突?比如用
hostname或spring.application.name做 key 前缀,避免不同服务实例误判同一 IP
滑动窗口逻辑本身不复杂,难的是和业务上下文绑定、和运维监控打通、以及在 Redis 故障时有降级策略(比如本地 ConcurrentHashMap + 时间戳桶,仅作兜底)。别指望一个注解包打天下,得根据接口重要性分层做限流。










