应定义语义明确的ratelimitexceededexception异常,携带限流键、已请求数、阈值、窗口时长等上下文;滑动窗口逻辑须独立完成清理、统计、判断三步原子操作后再抛异常;网关过滤器统一捕获该异常并返回429响应及标准限流头。

在网关中用自定义运行时异常配合滑动窗口实现平滑限流,关键不是“抛异常”本身,而是让异常成为限流决策的明确出口,并与滑动窗口的状态判断解耦、协同。它能避免隐式失败、统一错误响应、便于监控追踪,同时不破坏滑动窗口算法的时间精度和并发安全性。
自定义异常要承载明确语义
不要复用 RuntimeException 或泛化异常。应定义如 RateLimitExceededException,并携带必要上下文:
- 限流键(如
ip:192.168.1.100或user:U12345) - 当前窗口内已请求数、阈值、窗口时长
- 可选:预估重试时间(基于滑动窗口剩余有效期)
这样后续的全局异常处理器或日志切面才能精准分类、告警或降级,而不是只看到“请求被拒绝”。
滑动窗口逻辑必须独立于异常抛出时机
异常只是结果,不是控制流程的手段。滑动窗口的计数、清理、判断必须在无异常路径下完成:
- 每次请求到达时,先获取当前 key 对应的滑动窗口状态(例如从 Redis 或本地缓存读取带时间戳的请求记录列表)
- 执行“清理过期条目 + 统计有效请求数 + 判断是否超限”三步原子操作(推荐用 Lua 脚本操作 Redis,或用
ConcurrentHashMap + synchronized(deque)保护本地队列) - 仅当确认超限时,才构造并抛出
RateLimitExceededException
避免在清理或统计过程中因异常中断导致窗口状态错乱——这比限流不准更危险。
网关过滤器中统一拦截与响应转换
在 Spring Cloud Gateway 的 GlobalFilter 或自定义 GatewayFilter 中,捕获该异常并转为标准 HTTP 响应:
- 状态码固定为
429 Too Many Requests - 响应头添加
X-RateLimit-Limit、X-RateLimit-Remaining、Retry-After(可选)等标准字段 - 响应体返回 JSON 格式提示,含异常携带的上下文信息(生产环境可脱敏)
这样前端或调用方能识别限流状态并做重试策略,而不是当成服务端错误反复重发。
配合 Redis 实现分布式一致性
单机滑动窗口无法满足集群网关场景。推荐使用 Redis 存储每个 key 的时间戳列表(如用 Sorted Set),利用 ZREMRANGEBYSCORE 清理过期项、ZCARD 获取当前请求数:
- Lua 脚本封装整个滑动窗口判断逻辑,保证原子性
- 脚本返回 1 表示允许,0 表示拒绝;Java 层根据返回值决定是否抛出自定义异常
- Redis key 命名建议含网关实例标识(如
sliding:api:/order/create:ip:192.168.1.100),便于排查和清理
不依赖 Redis 过期机制(TTL 不够精确),而是靠滑动窗口自身时间范围裁剪,确保精度。










