公司的网关配了限流和降级两个 Filter。一次大促,流量触发了限流——RequestRateLimiter 返回了 429。按说限流拦截了就不该再走后续的 Filter 了。但 Hystrix 降级 Filter 也触发了,返回了 503。两个 Filter 同时想写响应,冲突了——客户端收到的状态码是 429,但 body 是 503 的降级 JSON。前端判断逻辑直接崩了。
Spring Cloud Gateway 的 Filter 是按责任链模式执行的,一个请求依次经过所有 Filter。问题在于:当一个 Filter 已经决定要拦截并返回了,它之后的 Filter 不知道前面已经拦截了,还在继续执行。
今天聊聊怎么给 Filter 设明确的优先级,让它们在冲突时按规则来,而不是同时抢着写响应。
责任链里的冲突怎么来的
Gateway 的 Filter 链执行顺序由 @Order 注解决定。默认情况下,Spring Cloud Gateway 的内置 Filter 顺序大致是:
NettyRoutingFilter(-1)
→ 自定义 Filter A(0)
→ 自定义 Filter B(0)
→ 自定义 Filter C(0)
→ 最后执行业务逻辑
如果你配了 RequestRateLimiter 和 CircuitBreaker,它们在同一个优先级上的行为是不确定的——这取决于 Spring 扫描 Bean 的顺序。不同环境、不同版本可能不一样。
而且更关键的是:即使限流 Filter 已经把响应设成了 429,Gateway 仍然会继续执行后面的 Filter——除非前面的 Filter 明确标记"到此为止"。
解法一:明确优先级,让关键拦截在前面
给每个 Filter 指定明确的 @Order:
@Order(-100) 限流 Filter —— 最先执行,不放行的直接返回
@Order(-90) 认证鉴权 Filter —— 没登录的不让过
@Order(-80) 降级熔断 Filter —— 服务挂了走降级
@Order(-70) 日志追踪 Filter —— 以上都过了才记录
@Order(0) 默认
限流必须排在最前面。如果限流已经拦了,后面的鉴权和降级都不需要执行。如果鉴权没过,降级也没必要触发——你都还没走到业务逻辑。
@Component
@Order(-100)
public class PriorityRateLimitFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isOverLimit(exchange)) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
// ★ 直接返回,不调 chain.filter,后面的 Filter 不再执行
return exchange.getResponse().setComplete();
}
return chain.filter(exchange); // 正常通过
}
}
关键是:被拦截时调用 response.setComplete() 而不是 chain.filter()。 后者会把请求传给下一个 Filter,前者直接在当前位置结束整个链路。
解法二:冲突检测——两个 Filter 都想写响应就报错
光靠优先级不能解决所有问题。如果两个 Filter 在同一个优先级上,又没有冲突检测,还是各写各的。
一个防御性的做法:在 Filter 写响应之前,检查响应是不是已经被前面的 Filter 写过了。
@Component
@Order(-80)
public class SafeCircuitBreakerFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 检查响应是否已被前面的 Filter 处理(如限流已返回 429)
if (exchange.getResponse().isCommitted()) {
log.warn("响应已被前置 Filter 提交,跳过降级逻辑");
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}
response.isCommitted() 检查响应头是否已经发送。如果前面已经有 Filter 写了响应,当前 Filter 就应该跳过。这是最低成本的冲突防护——不判断状态码对不对,只判断"有没有人已经动过响应了"。
实际部署中的建议
三种方式叠起来用:
Filter 执行流程:
限流 Filter (@Order -100)
├─ 过限 → 返回 429 + setComplete() → 停止
└─ 通过 → chain.filter() → 下一个
鉴权 Filter (@Order -90)
└─ response.isCommitted()? → 是 → 跳过
└─ chain.filter()
降级 Filter (@Order -80)
└─ response.isCommitted()? → 是 → 跳过
└─ chain.filter()
排序原则:拦截型的排前面,旁路型(日志、追踪)排后面。 拦截型的 "提前结束" 让旁路型不需要再判断 isCommitted。
总结
多个 Filter 抢着写响应,不是因为 Filter 太多了,而是因为没人管它们谁先谁后。
三条规则:
@Order明确优先级——限流必须排第一,降级排第二,日志追踪排最后- 拦截时直接
setComplete(),不调chain.filter()——后面 Filter 不执行 response.isCommitted()做冲突检测——有人写过响应了就跳过
配完之后,网关的 Filter 链从"谁先抢到算谁的"变成"按规矩排队"。
