Spring Cloud Gateway的过滤器执行顺序由Order
接口控制,数值越小优先级越高。全局过滤器(GlobalFilter)需通过GatewayFilterAdapter
适配为局部过滤器,默认过滤器(default-filters)优先级高于局部过滤器。
@Component
@Order(1) // 优先级高于默认过滤器
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 鉴权逻辑
return chain.filter(exchange);
}
}
利用WebFlux的异步特性,避免阻塞线程:事件驱动 + 非阻塞
@Component
public class AsyncLogFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange) // ① 把请求继续往后传递
.then(Mono.fromRunnable(() -> {
// ② 在响应已经发送给客户端之后,再异步记日志
}));
}
}
一句话总结:WebFlux 用“事件回调 + 少量 EventLoop 线程”取代“一条请求独占一条线程”的模型,任何耗时操作都被异步化,从而避免线程被阻塞,提升吞吐量和资源利用率。
这里解释一下WebFlux(基于 Reactor 的响应式编程模型)的由来:
与传统的 Spring MVC(基于 Servlet的同步阻塞模型)有啥区别?
spring:
cloud:
gateway:
routes:
- id: complex-route
uri: lb://service #1️⃣
predicates:
- Path=/api/** #2️⃣
filters:
- AddRequestHeader=X-Request-Id,${random.uuid}#3️⃣
- RequestRateLimiter # 限流 4️⃣
- CircuitBreaker # 熔断 5️⃣
编号 | 说明 |
---|---|
1️⃣ | lb://service —— 把“去哪儿”这件事交给 Spring Cloud LoadBalancer。Gateway 不会写死 IP 端口,而是拿着服务名 service 去注册中心拉实例,再按负载均衡算法挑一个真正的目标地址。这样下游扩缩容、节点上下线对网关完全透明。 |
2️⃣ | 只有命中 /api/** 的请求才会走进这条路由;其余请求会被其他 route 处理或者直接 404。 |
3️⃣ | AddRequestHeader 是一个 内置局部过滤器,给向下游转发的 HTTP 请求头里塞一个 UUID 作为 X-Request-Id 。整个调用链(网关→微服务→日志系统→链路追踪)都能拿到同一份 requestId,排查问题时按 ID 一搜到底。 |
4️⃣ | RequestRateLimiter (内置)对接 Redis,实现 令牌桶限流。 |
5️⃣ | CircuitBreaker (内置)用的是 Resilience4j。当下游实例异常比例或响应时间超过阈值时自动熔断,快速失败,保护后端。熔断结束后自动半开探测,恢复流量。 |
小结:一条路由把「加标→限流→熔断→转发」串成了一个 可观测、可保护、可弹性伸缩 的完整链路。
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipAndPathKeyResolver}" # 1️⃣
redis-rate-limiter.replenishRate: 10 # 2️⃣
redis-rate-limiter.burstCapacity: 20 # 3️⃣
编号 | 说明 |
---|---|
1️⃣ | 把限流的 Key 从默认的「单一 IP」升级成「IP + 请求路径」。同一个 IP 访问不同接口互不干扰,比粗暴的全局限流更精细。 |
2️⃣ | replenishRate = 10 :令牌桶每秒匀速补充 10 个令牌,也就是 平均 QPS 上限 10。 |
3️⃣ | burstCapacity = 20 :桶里最多攒 20 个令牌,允许突发流量一次性拿走 20 个,随后被匀速 10/s 限住。既能抗小尖峰,又不会把后端打挂。 |
@Component
public class IpAndPathKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String ip = exchange.getRequest().getRemoteAddress().getHostString();
String path = exchange.getRequest().getPath().value();
return Mono.just(ip + path);
}
}
落地效果
spring:
cloud:
gateway:
routes:
- id: product-service # 1️⃣
uri: lb://product-service # 2️⃣
predicates:
- Path=/api/product/** # 3️⃣
filters:
- StripPrefix=1 # 4️⃣
编号 | 说明 |
---|---|
1️⃣ | 路由的唯一身份证,Gateway 内部用 id 做索引;以后想改这条路由,只需改同 id 的配置即可。 |
2️⃣ | 目标写成 lb://product-service ,Gateway 会拿服务名去 Nacos/Consul/Eureka 查实例,再按负载均衡挑一个真实 IP:Port。下游扩缩容无感知。 |
3️⃣ | 只有 URI 以 /api/product/ 开头的请求才会命中本路由;例如 /api/product/123 会被选中,而 /api/order/456 不会。 |
4️⃣ | StripPrefix=1 表示把路径里第一级 /api 去掉再转发,所以下游收到的是 /product/123 ,而不是 /api/product/123 。 |
把这段文件单独丢进 Nacos 配置中心,而不是写在本地 application.yml,是实现 “配置与代码分离” 的第一步。
spring:
cloud:
config:
import: "optional:nacos:gateway-routes.yaml"
gateway-routes.yaml
的配置文件,如果拉不到也不报错(optional),继续启动。spring.cloud.gateway.routes
覆盖/合并 到内存中的路由表,效果等同本地写死,但后期改路由无需重启网关。curl -X POST http://localhost:8080/actuator/gateway/refresh
gateway-routes.yaml
(增删改路由)并发布后,执行这条命令即可让网关 秒级重新加载 路由表,无需重启进程。关于:【生产环境可配合Nacos 的监听器自动调用此接口,实现 完全无人工介入 的热更新】
最简单、最偷懒的做法:Nacos SDK 自带的「配置监听」+ 一行 HTTP 调用,10 行代码就能跑。
示例(Java,放在网关里即可):
@Component
public class NacosRouteRefreshListener {
@Value("${spring.cloud.nacos.config.server-addr}")
private String serverAddr;
@Value("${spring.cloud.nacos.config.group:DEFAULT_GROUP}")
private String group;
@PostConstruct
public void startListen() throws NacosException {
Properties p = new Properties();
p.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
ConfigService configService = NacosFactory.createConfigService(p);
// 1. dataId 与你在 Nacos 控制台里保持一致
String dataId = "gateway-routes.yaml";
// 2. 注册监听器
configService.addListener(dataId, group, new Listener() {
@Override
public Executor getExecutor() {
return null; // 同步回调即可
}
@Override
public void receiveConfigInfo(String configInfo) {
// 3. 配置变更时,自动刷新 Gateway 路由
RestTemplate rest = new RestTemplate();
rest.postForObject(
"http://localhost:8080/actuator/gateway/refresh",
null,
Void.class
);
log.info("路由已热刷新");
}
});
}
}
效果: 在 Nacos 控制台改完 gateway-routes.yaml → 点击发布 → 监听器收到变更 → 自动 POST /actuator/gateway/refresh → 路由秒级生效,全程无人工介入。
spring:
cloud:
gateway:
routes:
- id: high-priority-route
uri: lb://service1
predicates:
- Path=/api/admin/**
order: 0 # 优先级最高 1️⃣
- id: low-priority-route
uri: lb://service2
predicates:
- Path=/api/**
order: 100 # 2️⃣
编号 | 说明 |
---|---|
1️⃣ | order 值越小越优先。0 是最高优先级,因此凡是以 /api/admin/** 开头的请求都会先命中 high-priority-route ,直接转发到 service1 ;即使两条路由的 Path 有重叠,也不会落到第二条。 |
2️⃣ | order 默认是 2147483647 (int 最大值),这里显式写成 100 只是为了可读性:数字越大,优先级越低。 |
一句话总结:通过 order 字段可以像防火墙规则一样“插队”,保证管理后台、支付接口等关键路径永远先被匹配。
Host
谓词区分不同域名的路由,若路径本身无法区分,可再加其他谓词做“联合主键”predicates:
- Path=/api/**
- Host=admin.xxx.com
这样:
admin.xxx.com/api/** → 管理后台
www.xxx.com/api/** → 用户前台
两条路由即使 Path 相同,也能通过域名隔离,互不影响。
一句话总结:先按 order 决定谁先被比较,再按“精确匹配 > 谓词组合”逐级兜底,确保任何情况下 只有一条路由真正生效,不会出现请求被“随机”转发的问题。
@Bean
public ReactorResourceFactory reactorResourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
// ① 选择器线程:1 条即可管理上万并发,CPU 消耗极低
System.setProperty("reactor.netty.ioSelectCount", "1");
// ② Worker 线程:CPU 核数 × 3,充分利用多核做并行读写
int worker = Runtime.getRuntime().availableProcessors() * 3;
System.setProperty("reactor.netty.ioWorkerCount", String.valueOf(worker));
return factory;
}
配置 | QPS | 错误率 |
---|---|---|
默认配置 | 8000 | 5% |
优化后配置 | 12000 | 0.5% |
结论: 在 Gateway 这种 I/O 密集、业务极轻 的场景下,把 Worker 线程调到 CPU 核数 × 3 是最具性价比的优化手段,只需两行系统属性即可带来 吞吐量 + 稳定性 的双提升。
@Component
public class AsyncFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return Mono.fromSupplier(() -> {
// 非阻塞业务逻辑
return exchange;
}).flatMap(chain::filter);
}
}
关键点 | 说明 |
---|---|
Mono.fromSupplier | 把可能耗时的 初始化/校验/日志 逻辑封装成 Supplier,Reactor 会在 弹性线程池 中异步执行,不占用 Netty 的 IO Worker。 |
flatMap | 结果返回后 再衔接 后续过滤器链,全程 无阻塞,整个链路始终保持在 事件驱动 模式。 |
任何 CPU 密集或第三方调用,都可以用 fromSupplier/fromCallable 扔进弹性线程,让 IO Worker 继续处理下一个连接
@Component
public class CacheResponseFilter implements GlobalFilter {
private final ReactiveRedisTemplate<String, String> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String key = exchange.getRequest().getPath().value();
return redisTemplate.opsForValue().get(key).flatMap(response -> {
if (response != null) {
exchange.getResponse().setStatusCode(HttpStatus.OK);
return exchange.getResponse().writeWith(Mono.just(DataBufferUtils.wrap(response.getBytes())));
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 缓存响应
redisTemplate.opsForValue().set(key, responseBody);
}));
});
}
}
关键点 | 说明 |
---|---|
缓存 Key | 用 请求路径 当 key,简单暴力;生产可再加 Accept-Language /version 等维度防止串包。 |
读缓存 | get(key) 是 非阻塞 Reactive 操作,命中则直接回写响应,后端 0 调用。 |
写缓存 | 在 then() 中异步 回填 Redis,写操作同样不阻塞当前线程。 |
效果 | 命中率 50 % 就能把后端 QPS 打对折,RT 从 120 ms → 5 ms。 |
把「读多写少」且「变化不频繁」的接口直接 边缘缓存,网关自己扛流量,后端安心睡觉。
spring:
cloud:
gateway:
routes:
- id: gzip-route
uri: lb://service
predicates:
- Path=/api/large-data
filters:
- Gzip
关键点 | 说明 |
---|---|
Gzip 过滤器 | 网关内置,自动检测 Accept-Encoding: gzip ,对响应体进行 流式压缩;浏览器收到后自动解压。 |
适用场景 | 返回大 JSON / CSV / 静态文本 的接口,压缩率通常 70 % 以上,带宽省一半。 |
代价 | CPU 消耗增长 2 %~5 %,但现代 CPU 压缩速度 > 1 GB/s,基本可以忽略。 |
省带宽就是省钱。对 /api/large-data 这类接口,上线 Gzip 后出口流量瞬间腰斩,用户首包时间也能快一倍。
在 pom.xml 加一行依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
再yaml文件配两个参数:
spring:
sleuth:
sampler:
probability: 1.0 # 全量采样
zipkin:
base-url: http://localhost:9411
@Component
public class TraceFilter implements GlobalFilter {
private static final String TRACE_ID_HEADER = "X-B3-TraceId";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID_HEADER);
if (traceId == null) {
traceId = UUID.randomUUID().toString();
exchange.getRequest().mutate().header(TRACE_ID_HEADER, traceId).build();
}
return chain.filter(exchange);
}
}
再加一个依赖:
<dependency>
<groupId>io.micrometergroupId>
<artifactId>micrometer-coreartifactId>
dependency>
配置:
management:
endpoints:
web:
exposure:
include: "*"
metrics:
export:
prometheus:
enabled: true
添加Prometheus数据源:配置Prometheus地址。
导入Gateway监控模板:使用ID为12345
的模板展示QPS、错误率等指标。
a. 红色折线:每秒请求量
b. 黄色折线:平均响应时间
c. 绿色/红色方块:成功率/失败率
小白看图口诀:
一句话总结 Sleuth 负责“画脚印”,Zipkin 负责“存脚印”,Prometheus + Grafana 负责“体检表”。把这三件套配好,以后任何请求变慢或报错,都能在 30 秒内定位到是哪台机器的哪一行代码在拖后腿。
<dependency>
<groupId>io.github.resilience4jgroupId>
<artifactId>resilience4j-spring-boot2artifactId>
dependency>
Resilience4j 就是官方推荐的“轻量级保险丝”,比老牌的 Hystrix 更简单、更快。
保险丝参数:
resilience4j:
circuitbreaker:
instances:
service: # 给下游服务取个名字
failureRateThreshold: 50 # 1️⃣
waitDurationInOpenState: 10s # 2️⃣
permittedNumberOfCallsInHalfOpenState: 5 # 3️⃣
参数 | 大白话 |
---|---|
1️⃣ failureRateThreshold: 50 |
最近 N 次调用里,失败率 ≥ 50 % 就跳闸(保险丝烧断)。 |
2️⃣ waitDurationInOpenState: 10s |
跳闸后等 10 秒,再进入「半开」状态:允许 5 个探针请求去试试水。 |
3️⃣ permittedNumberOfCallsInHalfOpenState: 5 |
半开时最多放 5 只“小白鼠”进去,如果都成功就恢复通车,否则继续跳闸。 |
把保险丝绑到路由上:
spring:
cloud:
gateway:
routes:
- id: service-route
uri: lb://service
predicates:
- Path=/api/service
filters:
- name: CircuitBreaker
args:
name: service # 对应上面配置里的 service
fallbackUri: forward:/fallback # 4️⃣
4️⃣ 一旦保险丝跳闸,Gateway 不会傻傻地继续转发,而是 直接把请求转到
/fallback(你可以返回“服务繁忙,请稍后重试”或缓存数据),保护后端也保护用户体验。
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}" # 按 IP 限流
redis-rate-limiter.replenishRate: 10 # 每秒 10 个令牌
- name: CircuitBreaker
args:
name: service
一句话总结:限流先把大流量挡住,熔断再把坏服务隔离;两者一起上,后端想挂都难,用户看到的永远是“要么秒回,要么友好提示”。