Spring Cloud Gateway 核心原理
0 版本说明
本文主要讲解 Spring Cloud Gateway Server WebFlux,Spring Cloud Gateway 4.x 配置格式编写如下:
yaml
spring:
cloud:
gateway:
routes:
Spring Cloud Gateway 5.x 的配置前缀调整为:
yaml
spring:
cloud:
gateway:
server:
webflux:
routes:
当前官方文档同时维护 5.0.2、4.3.5、4.2.7 和 4.1.9 等稳定版本,实际项目必须根据 Spring Boot 与 Spring Cloud Release Train 的版本对应关系选择依赖和配置,不能直接照搬其他版本的配置。当前 4.3.5 和 5.0.2 官方 WebFlux Starter 均使用 spring-cloud-starter-gateway-server-webflux,较早的 4.2 及之前版本常见 spring-cloud-starter-gateway。
本文所有原理都围绕 Server WebFlux 展开,不讨论 Spring Cloud Gateway Server MVC,两者底层运行模型和部分配置方式不同。
1 为什么微服务项目需要 Gateway
单体项目只有一个后端服务,客户端可以直接访问;微服务项目可能包含用户、商品、订单、支付、文件等多个服务,如果客户端直接访问每个微服务,就需要维护所有服务的 IP、端口和接口地址。
#mermaid-svg-LilXSvUWipMfCR8X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LilXSvUWipMfCR8X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LilXSvUWipMfCR8X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LilXSvUWipMfCR8X .error-icon{fill:#552222;}#mermaid-svg-LilXSvUWipMfCR8X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LilXSvUWipMfCR8X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LilXSvUWipMfCR8X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LilXSvUWipMfCR8X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LilXSvUWipMfCR8X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LilXSvUWipMfCR8X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LilXSvUWipMfCR8X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LilXSvUWipMfCR8X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LilXSvUWipMfCR8X .marker.cross{stroke:#333333;}#mermaid-svg-LilXSvUWipMfCR8X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LilXSvUWipMfCR8X p{margin:0;}#mermaid-svg-LilXSvUWipMfCR8X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LilXSvUWipMfCR8X .cluster-label text{fill:#333;}#mermaid-svg-LilXSvUWipMfCR8X .cluster-label span{color:#333;}#mermaid-svg-LilXSvUWipMfCR8X .cluster-label span p{background-color:transparent;}#mermaid-svg-LilXSvUWipMfCR8X .label text,#mermaid-svg-LilXSvUWipMfCR8X span{fill:#333;color:#333;}#mermaid-svg-LilXSvUWipMfCR8X .node rect,#mermaid-svg-LilXSvUWipMfCR8X .node circle,#mermaid-svg-LilXSvUWipMfCR8X .node ellipse,#mermaid-svg-LilXSvUWipMfCR8X .node polygon,#mermaid-svg-LilXSvUWipMfCR8X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LilXSvUWipMfCR8X .rough-node .label text,#mermaid-svg-LilXSvUWipMfCR8X .node .label text,#mermaid-svg-LilXSvUWipMfCR8X .image-shape .label,#mermaid-svg-LilXSvUWipMfCR8X .icon-shape .label{text-anchor:middle;}#mermaid-svg-LilXSvUWipMfCR8X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LilXSvUWipMfCR8X .rough-node .label,#mermaid-svg-LilXSvUWipMfCR8X .node .label,#mermaid-svg-LilXSvUWipMfCR8X .image-shape .label,#mermaid-svg-LilXSvUWipMfCR8X .icon-shape .label{text-align:center;}#mermaid-svg-LilXSvUWipMfCR8X .node.clickable{cursor:pointer;}#mermaid-svg-LilXSvUWipMfCR8X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LilXSvUWipMfCR8X .arrowheadPath{fill:#333333;}#mermaid-svg-LilXSvUWipMfCR8X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LilXSvUWipMfCR8X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LilXSvUWipMfCR8X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LilXSvUWipMfCR8X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LilXSvUWipMfCR8X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LilXSvUWipMfCR8X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LilXSvUWipMfCR8X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LilXSvUWipMfCR8X .cluster text{fill:#333;}#mermaid-svg-LilXSvUWipMfCR8X .cluster span{color:#333;}#mermaid-svg-LilXSvUWipMfCR8X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LilXSvUWipMfCR8X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LilXSvUWipMfCR8X rect.text{fill:none;stroke-width:0;}#mermaid-svg-LilXSvUWipMfCR8X .icon-shape,#mermaid-svg-LilXSvUWipMfCR8X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LilXSvUWipMfCR8X .icon-shape p,#mermaid-svg-LilXSvUWipMfCR8X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LilXSvUWipMfCR8X .icon-shape .label rect,#mermaid-svg-LilXSvUWipMfCR8X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LilXSvUWipMfCR8X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LilXSvUWipMfCR8X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LilXSvUWipMfCR8X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
user-service
order-service
commodity-service
file-service
这种方式存在以下问题:
- 客户端需要维护大量服务地址,服务扩容、缩容或迁移后客户端也要修改。
- 登录认证、权限校验、限流、跨域、日志等逻辑会在多个微服务中重复实现。
- 微服务端口直接暴露,攻击者可能绕过统一入口访问内部服务。
- 无法统一实现灰度发布、熔断降级、流量控制和协议处理。
- 客户端与微服务拆分方式耦合,服务拆分或合并可能影响客户端。
因此,微服务架构通常增加统一入口:
#mermaid-svg-1pzKp5GPRRinWO8j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1pzKp5GPRRinWO8j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1pzKp5GPRRinWO8j .error-icon{fill:#552222;}#mermaid-svg-1pzKp5GPRRinWO8j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1pzKp5GPRRinWO8j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1pzKp5GPRRinWO8j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1pzKp5GPRRinWO8j .marker.cross{stroke:#333333;}#mermaid-svg-1pzKp5GPRRinWO8j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1pzKp5GPRRinWO8j p{margin:0;}#mermaid-svg-1pzKp5GPRRinWO8j .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1pzKp5GPRRinWO8j .cluster-label text{fill:#333;}#mermaid-svg-1pzKp5GPRRinWO8j .cluster-label span{color:#333;}#mermaid-svg-1pzKp5GPRRinWO8j .cluster-label span p{background-color:transparent;}#mermaid-svg-1pzKp5GPRRinWO8j .label text,#mermaid-svg-1pzKp5GPRRinWO8j span{fill:#333;color:#333;}#mermaid-svg-1pzKp5GPRRinWO8j .node rect,#mermaid-svg-1pzKp5GPRRinWO8j .node circle,#mermaid-svg-1pzKp5GPRRinWO8j .node ellipse,#mermaid-svg-1pzKp5GPRRinWO8j .node polygon,#mermaid-svg-1pzKp5GPRRinWO8j .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1pzKp5GPRRinWO8j .rough-node .label text,#mermaid-svg-1pzKp5GPRRinWO8j .node .label text,#mermaid-svg-1pzKp5GPRRinWO8j .image-shape .label,#mermaid-svg-1pzKp5GPRRinWO8j .icon-shape .label{text-anchor:middle;}#mermaid-svg-1pzKp5GPRRinWO8j .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1pzKp5GPRRinWO8j .rough-node .label,#mermaid-svg-1pzKp5GPRRinWO8j .node .label,#mermaid-svg-1pzKp5GPRRinWO8j .image-shape .label,#mermaid-svg-1pzKp5GPRRinWO8j .icon-shape .label{text-align:center;}#mermaid-svg-1pzKp5GPRRinWO8j .node.clickable{cursor:pointer;}#mermaid-svg-1pzKp5GPRRinWO8j .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1pzKp5GPRRinWO8j .arrowheadPath{fill:#333333;}#mermaid-svg-1pzKp5GPRRinWO8j .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1pzKp5GPRRinWO8j .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1pzKp5GPRRinWO8j .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pzKp5GPRRinWO8j .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1pzKp5GPRRinWO8j .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pzKp5GPRRinWO8j .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1pzKp5GPRRinWO8j .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1pzKp5GPRRinWO8j .cluster text{fill:#333;}#mermaid-svg-1pzKp5GPRRinWO8j .cluster span{color:#333;}#mermaid-svg-1pzKp5GPRRinWO8j div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1pzKp5GPRRinWO8j .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1pzKp5GPRRinWO8j rect.text{fill:none;stroke-width:0;}#mermaid-svg-1pzKp5GPRRinWO8j .icon-shape,#mermaid-svg-1pzKp5GPRRinWO8j .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1pzKp5GPRRinWO8j .icon-shape p,#mermaid-svg-1pzKp5GPRRinWO8j .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1pzKp5GPRRinWO8j .icon-shape .label rect,#mermaid-svg-1pzKp5GPRRinWO8j .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1pzKp5GPRRinWO8j .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1pzKp5GPRRinWO8j .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1pzKp5GPRRinWO8j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
Gateway
user-service
order-service
commodity-service
file-service
Spring Cloud Gateway 是 Spring Cloud 提供的 API 网关,可以将请求路由到后端服务,并统一处理安全、监控、限流和容错等横切逻辑。官方将其定位为路由 API 并提供安全、监控和弹性治理能力的网关。
| 能力 | 作用 |
|---|---|
| 路由转发 | 根据 Path、Method、Host、Header 等条件选择后端服务 |
| 服务发现 | 根据服务名从 Nacos、Eureka 等注册中心获取实例 |
| 负载均衡 | 从多个可用实例中选择一个实例 |
| 登录认证 | 判断请求携带的 Token 是否有效 |
| 权限控制 | 判断当前用户是否允许访问接口 |
| 请求限流 | 按用户、IP、接口、租户等维度限制流量 |
| 熔断降级 | 下游持续异常时快速失败或返回兜底结果 |
| 跨域处理 | 统一处理浏览器 CORS 请求 |
| 灰度发布 | 将部分用户或流量转发到新版本服务 |
| 日志监控 | 记录请求链路、状态码、耗时和异常 |
Gateway 是微服务的流量入口和通用治理层,不是业务服务,不应该在 Gateway 中实现下单、扣库存、支付等核心业务。
2 Gateway 在项目中的位置
生产环境中,Nacos 不属于业务请求链路,而是注册中心。微服务向 Nacos 注册,Gateway 从 Nacos 获取并维护服务实例列表;真正处理业务请求时,Gateway 会直接调用目标微服务,请求不会经过 Nacos。
#mermaid-svg-UBGdnYz2KJ4wRzjD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UBGdnYz2KJ4wRzjD .error-icon{fill:#552222;}#mermaid-svg-UBGdnYz2KJ4wRzjD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UBGdnYz2KJ4wRzjD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .marker.cross{stroke:#333333;}#mermaid-svg-UBGdnYz2KJ4wRzjD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UBGdnYz2KJ4wRzjD p{margin:0;}#mermaid-svg-UBGdnYz2KJ4wRzjD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster-label text{fill:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster-label span{color:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster-label span p{background-color:transparent;}#mermaid-svg-UBGdnYz2KJ4wRzjD .label text,#mermaid-svg-UBGdnYz2KJ4wRzjD span{fill:#333;color:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .node rect,#mermaid-svg-UBGdnYz2KJ4wRzjD .node circle,#mermaid-svg-UBGdnYz2KJ4wRzjD .node ellipse,#mermaid-svg-UBGdnYz2KJ4wRzjD .node polygon,#mermaid-svg-UBGdnYz2KJ4wRzjD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .rough-node .label text,#mermaid-svg-UBGdnYz2KJ4wRzjD .node .label text,#mermaid-svg-UBGdnYz2KJ4wRzjD .image-shape .label,#mermaid-svg-UBGdnYz2KJ4wRzjD .icon-shape .label{text-anchor:middle;}#mermaid-svg-UBGdnYz2KJ4wRzjD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .rough-node .label,#mermaid-svg-UBGdnYz2KJ4wRzjD .node .label,#mermaid-svg-UBGdnYz2KJ4wRzjD .image-shape .label,#mermaid-svg-UBGdnYz2KJ4wRzjD .icon-shape .label{text-align:center;}#mermaid-svg-UBGdnYz2KJ4wRzjD .node.clickable{cursor:pointer;}#mermaid-svg-UBGdnYz2KJ4wRzjD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .arrowheadPath{fill:#333333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UBGdnYz2KJ4wRzjD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UBGdnYz2KJ4wRzjD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UBGdnYz2KJ4wRzjD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster text{fill:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD .cluster span{color:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UBGdnYz2KJ4wRzjD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UBGdnYz2KJ4wRzjD rect.text{fill:none;stroke-width:0;}#mermaid-svg-UBGdnYz2KJ4wRzjD .icon-shape,#mermaid-svg-UBGdnYz2KJ4wRzjD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UBGdnYz2KJ4wRzjD .icon-shape p,#mermaid-svg-UBGdnYz2KJ4wRzjD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UBGdnYz2KJ4wRzjD .icon-shape .label rect,#mermaid-svg-UBGdnYz2KJ4wRzjD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UBGdnYz2KJ4wRzjD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UBGdnYz2KJ4wRzjD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UBGdnYz2KJ4wRzjD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务注册与心跳
服务注册与心跳
服务注册与心跳
服务发现与实例变更通知
浏览器或客户端
DNS
Nginx或云负载均衡
Gateway集群
user-service
order-service
commodity-service
Nacos注册中心
图中:
- 实线表示业务请求链路。
- 虚线表示服务注册与服务发现关系。
- Nacos 负责维护服务实例信息,不负责转发业务请求。
- Gateway 根据从 Nacos 获取的实例列表选择目标微服务。
完整请求过程如下:
user-service、order-service、commodity-service等微服务启动后,将自己的服务名、IP、端口等信息注册到 Nacos,并通过心跳或健康检查维持实例状态。- Gateway 通过 Nacos Discovery 订阅相关服务,获取并维护可用服务实例列表。
- 用户访问
https://example.com/api/orders/10001。 - DNS 将域名解析为 Nginx或云负载均衡的公网 IP。
- 浏览器与 Nginx 建立 HTTPS 连接,请求到达 443 端口。
- Nginx 将
/api请求负载均衡到某个 Gateway 实例。 - Gateway 根据 Path、Method、Header 等 Predicate 匹配 Route。
- Gateway 执行认证、权限校验、限流、路径重写等前置 Filter。
- Route 的目标地址为
lb://order-service时,Gateway 根据服务名获取order-service的可用实例列表。 - Spring Cloud LoadBalancer 从可用实例中选择一个实例。
- Gateway 将请求直接转发到选中的
order-service实例,请求不会经过 Nacos。 order-service执行业务逻辑,并访问 MySQL、Redis、RocketMQ 等基础组件。- 微服务将响应返回 Gateway,Gateway 执行后置 Filter。
- 响应经过 Gateway、Nginx 返回浏览器或客户端。
不会每次都查 Nacos
Gateway 通常不会在每一次业务请求到来时都实时查询 Nacos。Nacos 客户端会在本地维护服务实例信息,并在实例上线、下线或健康状态变化时更新实例列表;请求到达后,LoadBalancer 通常基于当前可用的本地实例列表选择实例。具体流程如下
order-service实例 Nacos Server 本地实例视图 Nacos DiscoveryClient Spring Cloud LoadBalancer Gateway 客户端 order-service实例 Nacos Server 本地实例视图 Nacos DiscoveryClient Spring Cloud LoadBalancer Gateway 客户端 #mermaid-svg-lhXS4fulQSezGFPI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lhXS4fulQSezGFPI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lhXS4fulQSezGFPI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lhXS4fulQSezGFPI .error-icon{fill:#552222;}#mermaid-svg-lhXS4fulQSezGFPI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lhXS4fulQSezGFPI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lhXS4fulQSezGFPI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lhXS4fulQSezGFPI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lhXS4fulQSezGFPI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lhXS4fulQSezGFPI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lhXS4fulQSezGFPI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lhXS4fulQSezGFPI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lhXS4fulQSezGFPI .marker.cross{stroke:#333333;}#mermaid-svg-lhXS4fulQSezGFPI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lhXS4fulQSezGFPI p{margin:0;}#mermaid-svg-lhXS4fulQSezGFPI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lhXS4fulQSezGFPI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lhXS4fulQSezGFPI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lhXS4fulQSezGFPI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lhXS4fulQSezGFPI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lhXS4fulQSezGFPI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lhXS4fulQSezGFPI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lhXS4fulQSezGFPI .sequenceNumber{fill:white;}#mermaid-svg-lhXS4fulQSezGFPI #sequencenumber{fill:#333;}#mermaid-svg-lhXS4fulQSezGFPI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lhXS4fulQSezGFPI .messageText{fill:#333;stroke:none;}#mermaid-svg-lhXS4fulQSezGFPI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lhXS4fulQSezGFPI .labelText,#mermaid-svg-lhXS4fulQSezGFPI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lhXS4fulQSezGFPI .loopText,#mermaid-svg-lhXS4fulQSezGFPI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lhXS4fulQSezGFPI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lhXS4fulQSezGFPI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lhXS4fulQSezGFPI .noteText,#mermaid-svg-lhXS4fulQSezGFPI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lhXS4fulQSezGFPI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lhXS4fulQSezGFPI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lhXS4fulQSezGFPI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lhXS4fulQSezGFPI .actorPopupMenu{position:absolute;}#mermaid-svg-lhXS4fulQSezGFPI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lhXS4fulQSezGFPI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lhXS4fulQSezGFPI .actor-man circle,#mermaid-svg-lhXS4fulQSezGFPI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lhXS4fulQSezGFPI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt本地已有order-service实例列表首次查询或本地没有实例列表 optorder-service实例发生变化 GET /api/orders/10001匹配到 lb://order-service为order-service选择实例获取order-service实例列表查询本地实例视图返回本地实例列表查询并订阅order-service返回当前可用实例列表保存order-service实例列表返回实例列表返回可用ServiceInstance列表根据负载均衡算法选择实例返回选中的10.0.0.1:8081直接发送业务请求返回业务响应返回响应通知或触发实例列表更新更新本地order-service实例视图
这张图表达的是:
- 第一次调用
order-service时,本地没有对应实例视图,Nacos DiscoveryClient 会查询并订阅该服务,然后保存实例列表。 - 后续请求通常直接读取本地维护的
order-service实例列表。 - Spring Cloud LoadBalancer 不负责向 Nacos Server 查询数据,它负责从候选实例中选择一个实例。
- Nacos DiscoveryClient 负责与 Nacos Server 通信,并维护本地实例视图。
- Gateway 拿到选中的实例地址后,直接请求
order-service,业务请求不经过 Nacos。
❓
order-service会调用user-service,它也需要负载均衡,它会缓存实例列表吗?会缓存谁的?order-service 调用 user-service 时,会作为服务消费者通过 Nacos DiscoveryClient 获取或订阅 user-service 的实例信息,再由 Spring Cloud LoadBalancer 选择一个实例。默认懒加载时,这一过程通常在第一次对该服务发起负载均衡调用时触发;后续请求主要使用本地维护的服务实例视图。它不会因为自己注册为 order-service 就自动获取所有 order-service 实例,只有在主动查询或调用该服务名时才需要对应实例列表(基本不会,谁会自己调自己呢)。
Gateway 在 Nacos 服务发现模型中与普通微服务基本相同,也可以同时作为服务提供者和消费者。区别是 Gateway 通常把请求需要转发到多个下游服务,所以会维护多个下游服务的实例视图。只供系统内部使用的服务不需要在网关配置路由,可以由其他微服务通过 Nacos 和 LoadBalancer 直接调用;如果这个服务只通过 MQ 消费消息,都不需要注册到 Nacos。
❓ Gateway 一定要配置 Nacos 吗?Spring Cloud Gateway 本身不要求使用 Nacos。可以使用固定 URI,也可以接入 Nacos、Eureka、Consul、Zookeeper或 Kubernetes。只有使用
lb://service-name进行客户端负载均衡时,才需要某种服务发现实现和 Spring Cloud LoadBalancer。❓ Gateway 自身能做服务多实例转发 + 负载均衡吗?
可以,Nacos 作用是什么?服务发现,Gateway 本身做的转发 + 负载均衡,Gateway 不依赖其他组件的时候配置如下
yamlspring: cloud: discovery: # 发现实例的方式 client: simple: instances: order-service: - uri: http://10.0.0.1:8081 - uri: http://10.0.0.2:8081 - uri: http://10.0.0.3:8081 # 多个实例,但是是固定 URI gateway: routes: - id: order-service-route uri: lb://order-service # 负载均衡 predicates: - Path=/api/orders/**
看一下 pom.xml 就知道了,Spring Cloud Gateway 的服务发现和负载均衡并不是全部由 Gateway 自己实现的。Gateway Starter 提供路由、过滤器和代理转发能力;Nacos Discovery Starter 提供服务注册与发现实现;Spring Cloud LoadBalancer 负责从 DiscoveryClient 提供的候选实例中选择一个实例。
xml
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
3 Nginx、Gateway 和 Zuul 的区别
3.1 Nginx 和 Gateway
| 对比项 | Nginx | Spring Cloud Gateway |
|---|---|---|
| 核心定位 | 通用反向代理服务器 | 微服务 API 网关 |
| 常见位置 | 系统最外层 | Nginx 与微服务之间 |
| 主要职责 | HTTPS、静态资源、反向代理、入口负载均衡 | 路由、鉴权、限流、服务发现、请求转发 |
| 后端地址 | 通常手动配置或通过 DNS 获取 | 可通过 Nacos 等注册中心发现实例 |
| 负载均衡 | 在配置好的后端地址中选择 | LoadBalancer 在服务实例中选择 |
| 扩展方式 | 配置、模块或 Lua | Java Filter |
| 适合规则 | 域名、路径、IP 等网络规则 | Token、权限、用户、租户等业务规则 |
| 性能 | 通常更高 | 可以满足大多数微服务入口场景 |
#mermaid-svg-yKp1YaxOqrvC2dYp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yKp1YaxOqrvC2dYp .error-icon{fill:#552222;}#mermaid-svg-yKp1YaxOqrvC2dYp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yKp1YaxOqrvC2dYp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yKp1YaxOqrvC2dYp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yKp1YaxOqrvC2dYp .marker.cross{stroke:#333333;}#mermaid-svg-yKp1YaxOqrvC2dYp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yKp1YaxOqrvC2dYp p{margin:0;}#mermaid-svg-yKp1YaxOqrvC2dYp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster-label text{fill:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster-label span{color:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster-label span p{background-color:transparent;}#mermaid-svg-yKp1YaxOqrvC2dYp .label text,#mermaid-svg-yKp1YaxOqrvC2dYp span{fill:#333;color:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp .node rect,#mermaid-svg-yKp1YaxOqrvC2dYp .node circle,#mermaid-svg-yKp1YaxOqrvC2dYp .node ellipse,#mermaid-svg-yKp1YaxOqrvC2dYp .node polygon,#mermaid-svg-yKp1YaxOqrvC2dYp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yKp1YaxOqrvC2dYp .rough-node .label text,#mermaid-svg-yKp1YaxOqrvC2dYp .node .label text,#mermaid-svg-yKp1YaxOqrvC2dYp .image-shape .label,#mermaid-svg-yKp1YaxOqrvC2dYp .icon-shape .label{text-anchor:middle;}#mermaid-svg-yKp1YaxOqrvC2dYp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yKp1YaxOqrvC2dYp .rough-node .label,#mermaid-svg-yKp1YaxOqrvC2dYp .node .label,#mermaid-svg-yKp1YaxOqrvC2dYp .image-shape .label,#mermaid-svg-yKp1YaxOqrvC2dYp .icon-shape .label{text-align:center;}#mermaid-svg-yKp1YaxOqrvC2dYp .node.clickable{cursor:pointer;}#mermaid-svg-yKp1YaxOqrvC2dYp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yKp1YaxOqrvC2dYp .arrowheadPath{fill:#333333;}#mermaid-svg-yKp1YaxOqrvC2dYp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yKp1YaxOqrvC2dYp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yKp1YaxOqrvC2dYp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yKp1YaxOqrvC2dYp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yKp1YaxOqrvC2dYp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yKp1YaxOqrvC2dYp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster text{fill:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp .cluster span{color:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yKp1YaxOqrvC2dYp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yKp1YaxOqrvC2dYp rect.text{fill:none;stroke-width:0;}#mermaid-svg-yKp1YaxOqrvC2dYp .icon-shape,#mermaid-svg-yKp1YaxOqrvC2dYp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yKp1YaxOqrvC2dYp .icon-shape p,#mermaid-svg-yKp1YaxOqrvC2dYp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yKp1YaxOqrvC2dYp .icon-shape .label rect,#mermaid-svg-yKp1YaxOqrvC2dYp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yKp1YaxOqrvC2dYp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yKp1YaxOqrvC2dYp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yKp1YaxOqrvC2dYp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静态资源
API请求
客户端
Nginx
HTML JS CSS
Gateway
微服务
Nginx 怎么配置 Gateway 网关负载均衡呢?
- 可以对网关搞个域名,用 DNS 解析多个域名
- 用 Kubernetes 或者云负载均衡,等等~反正肯定不会写死在配置文件
Nginx 负责让请求高效、安全地进入系统,更偏网络层和基础设施层;Gateway 负责请求进入微服务系统后,应该经过哪些规则并转发到哪个服务实例,更偏微服务治理和 Java 应用层。
3.2 Gateway 和 Zuul
| 对比项 | Zuul 1.x | Spring Cloud Gateway Server WebFlux |
|---|---|---|
| 网络模型 | Servlet 阻塞模型 | 异步非阻塞模型 |
| 底层运行时 | Servlet 容器 | Reactor Netty |
| 编程模型 | 同步调用 | Reactor 响应式编程 |
| 高并发 I/O | 需要较多线程等待 | 更适合大量网络 I/O |
| Spring Cloud 生态 | 已不是新项目的主流选择 | Spring Cloud 官方重点维护 |
Zuul 2.x 也采用 Netty 非阻塞模型,但没有成为 Spring Cloud Netflix 的核心正式集成组件,因此 Spring Cloud 新项目通常优先选择 Gateway。
在传统 Servlet 阻塞模型中,如果当前线程使用阻塞式客户端调用 order-service,下游 100ms 后返回,那么该线程会被占用并等待 100ms,期间不能处理其他请求。
在响应式非阻塞模型中,线程发出网络请求后,注册"响应回来后继续执行什么",随后释放执行权,去处理其他请求;等下游响应到达,Netty 再通过事件通知继续执行后面的处理逻辑。
4 Gateway 的三大核心概念
Spring Cloud Gateway 最重要的三个概念是:
Route:一条完整的路由规则。Predicate:判断当前请求是否满足路由条件。Filter:在请求转发前后执行处理逻辑。
#mermaid-svg-rDpAn0okSqHzrlmR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rDpAn0okSqHzrlmR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rDpAn0okSqHzrlmR .error-icon{fill:#552222;}#mermaid-svg-rDpAn0okSqHzrlmR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rDpAn0okSqHzrlmR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rDpAn0okSqHzrlmR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rDpAn0okSqHzrlmR .marker.cross{stroke:#333333;}#mermaid-svg-rDpAn0okSqHzrlmR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rDpAn0okSqHzrlmR p{margin:0;}#mermaid-svg-rDpAn0okSqHzrlmR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rDpAn0okSqHzrlmR .cluster-label text{fill:#333;}#mermaid-svg-rDpAn0okSqHzrlmR .cluster-label span{color:#333;}#mermaid-svg-rDpAn0okSqHzrlmR .cluster-label span p{background-color:transparent;}#mermaid-svg-rDpAn0okSqHzrlmR .label text,#mermaid-svg-rDpAn0okSqHzrlmR span{fill:#333;color:#333;}#mermaid-svg-rDpAn0okSqHzrlmR .node rect,#mermaid-svg-rDpAn0okSqHzrlmR .node circle,#mermaid-svg-rDpAn0okSqHzrlmR .node ellipse,#mermaid-svg-rDpAn0okSqHzrlmR .node polygon,#mermaid-svg-rDpAn0okSqHzrlmR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rDpAn0okSqHzrlmR .rough-node .label text,#mermaid-svg-rDpAn0okSqHzrlmR .node .label text,#mermaid-svg-rDpAn0okSqHzrlmR .image-shape .label,#mermaid-svg-rDpAn0okSqHzrlmR .icon-shape .label{text-anchor:middle;}#mermaid-svg-rDpAn0okSqHzrlmR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rDpAn0okSqHzrlmR .rough-node .label,#mermaid-svg-rDpAn0okSqHzrlmR .node .label,#mermaid-svg-rDpAn0okSqHzrlmR .image-shape .label,#mermaid-svg-rDpAn0okSqHzrlmR .icon-shape .label{text-align:center;}#mermaid-svg-rDpAn0okSqHzrlmR .node.clickable{cursor:pointer;}#mermaid-svg-rDpAn0okSqHzrlmR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rDpAn0okSqHzrlmR .arrowheadPath{fill:#333333;}#mermaid-svg-rDpAn0okSqHzrlmR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rDpAn0okSqHzrlmR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rDpAn0okSqHzrlmR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rDpAn0okSqHzrlmR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rDpAn0okSqHzrlmR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rDpAn0okSqHzrlmR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rDpAn0okSqHzrlmR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rDpAn0okSqHzrlmR .cluster text{fill:#333;}#mermaid-svg-rDpAn0okSqHzrlmR .cluster span{color:#333;}#mermaid-svg-rDpAn0okSqHzrlmR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rDpAn0okSqHzrlmR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rDpAn0okSqHzrlmR rect.text{fill:none;stroke-width:0;}#mermaid-svg-rDpAn0okSqHzrlmR .icon-shape,#mermaid-svg-rDpAn0okSqHzrlmR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rDpAn0okSqHzrlmR .icon-shape p,#mermaid-svg-rDpAn0okSqHzrlmR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rDpAn0okSqHzrlmR .icon-shape .label rect,#mermaid-svg-rDpAn0okSqHzrlmR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rDpAn0okSqHzrlmR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rDpAn0okSqHzrlmR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rDpAn0okSqHzrlmR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
客户端请求
Predicate是否匹配
继续匹配其他Route
确定Route和目标URI
执行Filter Chain
转发到微服务
官方对三者的定义是:Route 由 ID、目标 URI、Predicate 集合和 Filter 集合组成;聚合 Predicate 为 true 时 Route 匹配;Filter 可以在下游请求发送前后修改请求和响应。
yaml
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: auth
uri: lb://idle-store-auth
predicates:
- Path=/auth/**
filters:
...
可以将一条 Route 理解为:
满足什么条件 + 转发到什么地方 + 转发前后做什么处理。
4.1 Route 路由
Route 表示一条完整的路由规则,主要包含以下属性:
| 属性 | 含义 |
|---|---|
id |
路由唯一标识 |
uri |
请求最终转发到哪里 |
predicates |
当前请求需要满足的匹配条件 |
filters |
请求转发前后执行的处理逻辑 |
order |
Route 的匹配优先级,数值越小优先级越高 |
metadata |
路由自定义元数据,例如超时、分组信息 |
yaml
spring:
cloud:
gateway:
routes:
- id: user-service-route
order: 0
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1 # 去掉 /api 前缀
这条 Route 表示:请求路径匹配 /api/users/** 时,删除第一层路径,然后通过负载均衡转发到 user-service。
4.2 Predicate 断言
Predicate 用来判断当前请求是否匹配某条 Route。它可以根据请求路径、请求方法、请求头、Cookie、请求参数、客户端 IP 等信息进行判断。
yaml
predicates:
- Path=/api/users/**
- Method=GET
上述配置表示,请求必须同时满足以下条件:
- 请求路径匹配
/api/users/**。 - 请求方法为
GET。
只有全部条件都满足,当前 Route 才会匹配成功。
4.2.1 一个 Route 可以配置多个 Predicate
一条 Route 可以同时配置多个 Predicate,多个 Predicate 默认是 AND 关系,必须全部满足。
yaml
predicates:
- Path=/api/users/**
- Method=GET
- Header=X-Source, app
这条 Route 要求请求同时满足:
- 路径匹配
/api/users/**。 - 请求方法为
GET。 - 请求头
X-Source的值匹配app。
#mermaid-svg-ykCPgkE7pGE9FhGm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ykCPgkE7pGE9FhGm .error-icon{fill:#552222;}#mermaid-svg-ykCPgkE7pGE9FhGm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ykCPgkE7pGE9FhGm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ykCPgkE7pGE9FhGm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ykCPgkE7pGE9FhGm .marker.cross{stroke:#333333;}#mermaid-svg-ykCPgkE7pGE9FhGm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ykCPgkE7pGE9FhGm p{margin:0;}#mermaid-svg-ykCPgkE7pGE9FhGm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster-label text{fill:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster-label span{color:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster-label span p{background-color:transparent;}#mermaid-svg-ykCPgkE7pGE9FhGm .label text,#mermaid-svg-ykCPgkE7pGE9FhGm span{fill:#333;color:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm .node rect,#mermaid-svg-ykCPgkE7pGE9FhGm .node circle,#mermaid-svg-ykCPgkE7pGE9FhGm .node ellipse,#mermaid-svg-ykCPgkE7pGE9FhGm .node polygon,#mermaid-svg-ykCPgkE7pGE9FhGm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ykCPgkE7pGE9FhGm .rough-node .label text,#mermaid-svg-ykCPgkE7pGE9FhGm .node .label text,#mermaid-svg-ykCPgkE7pGE9FhGm .image-shape .label,#mermaid-svg-ykCPgkE7pGE9FhGm .icon-shape .label{text-anchor:middle;}#mermaid-svg-ykCPgkE7pGE9FhGm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ykCPgkE7pGE9FhGm .rough-node .label,#mermaid-svg-ykCPgkE7pGE9FhGm .node .label,#mermaid-svg-ykCPgkE7pGE9FhGm .image-shape .label,#mermaid-svg-ykCPgkE7pGE9FhGm .icon-shape .label{text-align:center;}#mermaid-svg-ykCPgkE7pGE9FhGm .node.clickable{cursor:pointer;}#mermaid-svg-ykCPgkE7pGE9FhGm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ykCPgkE7pGE9FhGm .arrowheadPath{fill:#333333;}#mermaid-svg-ykCPgkE7pGE9FhGm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ykCPgkE7pGE9FhGm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ykCPgkE7pGE9FhGm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ykCPgkE7pGE9FhGm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ykCPgkE7pGE9FhGm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ykCPgkE7pGE9FhGm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster text{fill:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm .cluster span{color:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ykCPgkE7pGE9FhGm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ykCPgkE7pGE9FhGm rect.text{fill:none;stroke-width:0;}#mermaid-svg-ykCPgkE7pGE9FhGm .icon-shape,#mermaid-svg-ykCPgkE7pGE9FhGm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ykCPgkE7pGE9FhGm .icon-shape p,#mermaid-svg-ykCPgkE7pGE9FhGm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ykCPgkE7pGE9FhGm .icon-shape .label rect,#mermaid-svg-ykCPgkE7pGE9FhGm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ykCPgkE7pGE9FhGm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ykCPgkE7pGE9FhGm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ykCPgkE7pGE9FhGm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
请求进入
Path 是否匹配
当前 Route 匹配失败
Method 是否为 GET
Header 是否匹配
Route 匹配成功
例如,下面的请求可以匹配成功:
http
GET /api/users/100
X-Source: app
下面的请求无法匹配:
http
POST /api/users/100
X-Source: app
因为请求方法不是 GET。
一个 Route 中的多个 Predicate 默认是 AND 关系,而不是满足任意一个即可。
4.2.2 多个 Predicate 的组合关系
在 YAML 中,同一条 Route 配置的多个 Predicate 默认是 AND 关系,必须全部满足才能匹配成功。
YAML 不支持通用的 OR 和 NOT 组合。需要使用复杂逻辑时,可以改用 Java DSL:
java
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-route", r -> r // id
.path("/api/users/**") // path
.or()
.path("/api/admin/**")
.uri("lb://user-service")) // uri
.build();
}
上述配置表示:
text
Path 匹配 /api/users/** || Path 匹配 /api/admin/**
Java DSL 中:
and():两个条件都要满足。or():满足任意一个条件即可。negate():对前面的条件取反。
YAML 中多个 Predicate 默认按 AND 组合;需要 OR、NOT 等复杂组合时,可以使用 Java DSL。
4.2.3 一个请求匹配多个 Route 怎么办
Gateway 不会把同一个请求同时转发到所有匹配 Route,而是:
CachingRouteLocator按 Route 的order排序。RoutePredicateHandlerMapping按顺序判断每条 Route 的 Predicate。- 通过 Reactor 的
.next()取得第一个匹配成功的 Route。
因此,order 越小的 Route 越先判断;多个 Route 都能匹配时,选择排序后第一个匹配成功的 Route。
yaml
spring:
cloud:
gateway:
routes:
- id: user-detail-route
order: -10
uri: lb://user-service
predicates:
- Path=/api/users/{id}
- id: common-api-route
order: 0
uri: lb://common-service
predicates:
- Path=/api/**
请求 /api/users/1 同时满足两条 Route,但 user-detail-route 的 order=-10,因此优先匹配。
多条 Route 的
order相同时,不建议依赖 YAML 书写顺序,应显式设置不同的order,避免路由优先级产生歧义。
4.2.4 常见 Predicate
| Predicate | 作用 |
|---|---|
Path |
根据请求路径匹配 |
Method |
根据 GET、POST 等方法匹配 |
Host |
根据 Host 或域名匹配 |
Header |
根据请求头和正则表达式匹配 |
Query |
根据查询参数匹配 |
Cookie |
根据 Cookie 匹配 |
RemoteAddr |
根据网络连接的远程 IP 匹配 |
XForwardedRemoteAddr |
根据可信代理传递的客户端 IP 匹配 |
After |
指定时间之后生效 |
Before |
指定时间之前生效 |
Between |
指定时间范围内生效 |
Weight |
在同一分组中按照权重分配流量 |
例如:
yaml
predicates:
- Path=/api/orders/**
- Method=GET
- Header=X-Client-Version, 2\..*
请求路径、请求方法和客户端版本必须同时匹配,才能进入当前 Route。
4.3 Filter 过滤器
Filter 可以在请求转发前和响应返回后执行处理逻辑。
#mermaid-svg-YXcoRJ27BClA6PdI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YXcoRJ27BClA6PdI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YXcoRJ27BClA6PdI .error-icon{fill:#552222;}#mermaid-svg-YXcoRJ27BClA6PdI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YXcoRJ27BClA6PdI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YXcoRJ27BClA6PdI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YXcoRJ27BClA6PdI .marker.cross{stroke:#333333;}#mermaid-svg-YXcoRJ27BClA6PdI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YXcoRJ27BClA6PdI p{margin:0;}#mermaid-svg-YXcoRJ27BClA6PdI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YXcoRJ27BClA6PdI .cluster-label text{fill:#333;}#mermaid-svg-YXcoRJ27BClA6PdI .cluster-label span{color:#333;}#mermaid-svg-YXcoRJ27BClA6PdI .cluster-label span p{background-color:transparent;}#mermaid-svg-YXcoRJ27BClA6PdI .label text,#mermaid-svg-YXcoRJ27BClA6PdI span{fill:#333;color:#333;}#mermaid-svg-YXcoRJ27BClA6PdI .node rect,#mermaid-svg-YXcoRJ27BClA6PdI .node circle,#mermaid-svg-YXcoRJ27BClA6PdI .node ellipse,#mermaid-svg-YXcoRJ27BClA6PdI .node polygon,#mermaid-svg-YXcoRJ27BClA6PdI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YXcoRJ27BClA6PdI .rough-node .label text,#mermaid-svg-YXcoRJ27BClA6PdI .node .label text,#mermaid-svg-YXcoRJ27BClA6PdI .image-shape .label,#mermaid-svg-YXcoRJ27BClA6PdI .icon-shape .label{text-anchor:middle;}#mermaid-svg-YXcoRJ27BClA6PdI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YXcoRJ27BClA6PdI .rough-node .label,#mermaid-svg-YXcoRJ27BClA6PdI .node .label,#mermaid-svg-YXcoRJ27BClA6PdI .image-shape .label,#mermaid-svg-YXcoRJ27BClA6PdI .icon-shape .label{text-align:center;}#mermaid-svg-YXcoRJ27BClA6PdI .node.clickable{cursor:pointer;}#mermaid-svg-YXcoRJ27BClA6PdI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YXcoRJ27BClA6PdI .arrowheadPath{fill:#333333;}#mermaid-svg-YXcoRJ27BClA6PdI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YXcoRJ27BClA6PdI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YXcoRJ27BClA6PdI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YXcoRJ27BClA6PdI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YXcoRJ27BClA6PdI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YXcoRJ27BClA6PdI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YXcoRJ27BClA6PdI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YXcoRJ27BClA6PdI .cluster text{fill:#333;}#mermaid-svg-YXcoRJ27BClA6PdI .cluster span{color:#333;}#mermaid-svg-YXcoRJ27BClA6PdI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YXcoRJ27BClA6PdI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YXcoRJ27BClA6PdI rect.text{fill:none;stroke-width:0;}#mermaid-svg-YXcoRJ27BClA6PdI .icon-shape,#mermaid-svg-YXcoRJ27BClA6PdI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YXcoRJ27BClA6PdI .icon-shape p,#mermaid-svg-YXcoRJ27BClA6PdI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YXcoRJ27BClA6PdI .icon-shape .label rect,#mermaid-svg-YXcoRJ27BClA6PdI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YXcoRJ27BClA6PdI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YXcoRJ27BClA6PdI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YXcoRJ27BClA6PdI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端请求
执行Pre逻辑
调用下游服务
执行Post逻辑
返回客户端
4.3.1 Pre 和 Post 不是两个独立接口
Gateway 没有分别叫作 PreFilter 和 PostFilter 的核心接口,Pre 和 Post 表示同一个 Filter 中位于 chain.filter(exchange) 前后的两个执行阶段。
java
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
// Pre:请求继续向后执行之前
long startTime = System.currentTimeMillis();
return chain.filter(exchange)
.doFinally(signalType -> {
// Post:后续处理结束之后
long cost = System.currentTimeMillis() - startTime;
});
}
| 阶段 | 常见用途 |
|---|---|
| Pre | 鉴权、限流、参数校验、路径修改、添加 Header |
| Post | 修改响应头、记录状态码、统计耗时、清理上下文 |
4.3.2 Filter 按作用范围分类
Gateway 中的 Filter 可以简单分成三种使用方式:
| 类型 | 作用范围 | 怎么配置 |
|---|---|---|
| Route Filter | 只对指定 Route 生效 | 写在某条 Route 的 filters 中 |
| Default Filter | 自动添加到所有 Route | 写在 default-filters 中 |
| GlobalFilter | 对所有已经匹配到 Route 的请求生效 | 编写 Java 类实现 GlobalFilter |
Route Filter:只对某条 Route 生效
yaml
spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
这里的 StripPrefix=1 只属于 user-service-route。其他 Route 不受影响。
Default Filter:自动应用到所有 Route
yaml
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Gateway, Spring-Cloud-Gateway
它等价于给每一条 Route 都添加了这个 Filter。
例如系统中有:
user-service-routeorder-service-routecommodity-service-route
这三条 Route 的响应中都会添加:
text
X-Gateway: Spring-Cloud-Gateway
GlobalFilter:用 Java 编写全局过滤逻辑
当过滤逻辑比较复杂,需要写 Java 代码时,可以实现 GlobalFilter。
常见用途包括:
- 登录校验;
- 记录请求日志;
- 生成 TraceId;
- IP 黑白名单;
- 清理客户端伪造的请求头。
java
@Component
public class TraceGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log =
LoggerFactory.getLogger(TraceGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
String traceId = UUID.randomUUID()
.toString()
.replace("-", "");
ServerHttpRequest request = exchange.getRequest()
.mutate()
.header("X-Trace-Id", traceId)
.build();
ServerWebExchange newExchange = exchange.mutate()
.request(request)
.build();
return chain.filter(newExchange)
.doFinally(signalType -> {
long cost =
System.currentTimeMillis() - startTime;
log.info(
"traceId={}, path={}, status={}, cost={}ms",
traceId,
request.getPath(),
newExchange.getResponse().getStatusCode(),
cost
);
});
}
@Override
public int getOrder() {
return -100;
}
}
这段代码做了三件事:
- 请求进入时生成一个
traceId。 - 把
traceId放进请求头,再传给下游微服务。 - 请求结束后记录路径、状态码和耗时。
执行流程如下:
#mermaid-svg-jXIdszxIpBX6y7DJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jXIdszxIpBX6y7DJ .error-icon{fill:#552222;}#mermaid-svg-jXIdszxIpBX6y7DJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jXIdszxIpBX6y7DJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jXIdszxIpBX6y7DJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jXIdszxIpBX6y7DJ .marker.cross{stroke:#333333;}#mermaid-svg-jXIdszxIpBX6y7DJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jXIdszxIpBX6y7DJ p{margin:0;}#mermaid-svg-jXIdszxIpBX6y7DJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster-label text{fill:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster-label span{color:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster-label span p{background-color:transparent;}#mermaid-svg-jXIdszxIpBX6y7DJ .label text,#mermaid-svg-jXIdszxIpBX6y7DJ span{fill:#333;color:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ .node rect,#mermaid-svg-jXIdszxIpBX6y7DJ .node circle,#mermaid-svg-jXIdszxIpBX6y7DJ .node ellipse,#mermaid-svg-jXIdszxIpBX6y7DJ .node polygon,#mermaid-svg-jXIdszxIpBX6y7DJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jXIdszxIpBX6y7DJ .rough-node .label text,#mermaid-svg-jXIdszxIpBX6y7DJ .node .label text,#mermaid-svg-jXIdszxIpBX6y7DJ .image-shape .label,#mermaid-svg-jXIdszxIpBX6y7DJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-jXIdszxIpBX6y7DJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jXIdszxIpBX6y7DJ .rough-node .label,#mermaid-svg-jXIdszxIpBX6y7DJ .node .label,#mermaid-svg-jXIdszxIpBX6y7DJ .image-shape .label,#mermaid-svg-jXIdszxIpBX6y7DJ .icon-shape .label{text-align:center;}#mermaid-svg-jXIdszxIpBX6y7DJ .node.clickable{cursor:pointer;}#mermaid-svg-jXIdszxIpBX6y7DJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jXIdszxIpBX6y7DJ .arrowheadPath{fill:#333333;}#mermaid-svg-jXIdszxIpBX6y7DJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jXIdszxIpBX6y7DJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jXIdszxIpBX6y7DJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jXIdszxIpBX6y7DJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jXIdszxIpBX6y7DJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jXIdszxIpBX6y7DJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster text{fill:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ .cluster span{color:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jXIdszxIpBX6y7DJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jXIdszxIpBX6y7DJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-jXIdszxIpBX6y7DJ .icon-shape,#mermaid-svg-jXIdszxIpBX6y7DJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jXIdszxIpBX6y7DJ .icon-shape p,#mermaid-svg-jXIdszxIpBX6y7DJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jXIdszxIpBX6y7DJ .icon-shape .label rect,#mermaid-svg-jXIdszxIpBX6y7DJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jXIdszxIpBX6y7DJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jXIdszxIpBX6y7DJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jXIdszxIpBX6y7DJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求匹配到 Route
进入 GlobalFilter
生成 TraceId
添加请求头
继续执行其他 Filter
转发到微服务
响应返回
记录状态码和耗时
GlobalFilter 所谓"全局",是指:
只要请求已经匹配到某条 Gateway Route,它通常都会参与该 Route 的过滤器链。某些没有匹配到 Route、直接返回 404 的请求是不会执行的。
三者之间的关系
实际执行顺序是会把 GlobalFilter 和当前 Route 的 GatewayFilter、Default Filter 合并后,再根据 order 排序执行,从小到大执行。
4.3.3 Filter 执行顺序
Gateway 会把 GlobalFilter、default-filters 和当前 Route 的 filters 合并,然后统一按照 order 从小到大排序。
YAML Filter 的默认顺序
没有自带顺序的 YAML Filter,会按照配置位置获得 1、2、3...... 的 order:
yaml
default-filters:
- AddRequestHeader=X-Gateway, gateway # order=1
- RemoveRequestHeader=X-Test # order=2
routes:
- id: user-route
uri: lb://user-service
filters:
- StripPrefix=1 # order=1
- AddRequestHeader=X-Service, user # order=2
default-filters 和 Route 的 filters 会分别从 1 开始编号。部分内置 Filter 自己实现了 Ordered,此时使用其内部定义的顺序。
几个重要的内置 GlobalFilter 顺序如下:
| Filter | order | 作用 |
|---|---|---|
NettyWriteResponseFilter |
-1 |
最后把响应写回客户端 (先进入它,然后它在 post 写入响应) |
| 普通 YAML Filter | 通常从 1 开始 |
修改路径、Header、重试等 |
RouteToRequestUrlFilter |
10000 |
根据 Route 生成目标 URL |
ReactiveLoadBalancerClientFilter |
10150 |
选择服务实例 |
NettyRoutingFilter |
Integer.MAX_VALUE |
真正向下游发送网络请求 |
大致请求顺序为:
text
自定义 GlobalFilter
→ YAML Filter
→ 生成目标 URL
→ 负载均衡选择实例
→ Netty 转发请求
自定义 GlobalFilter 怎么设置
java
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
// pre:转发请求之前执行
System.out.println("开始鉴权");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// post:下游响应返回之后执行
System.out.println("响应返回");
}));
}
@Override
public int getOrder() {
return -100;
}
}
常用取值可以这样理解:
order |
执行位置 |
|---|---|
-200、-100 |
很早执行,适合 TraceId、鉴权、黑白名单 |
0 |
通常早于 YAML Filter |
1、2、3 |
可能与 YAML Filter 顺序冲突,不建议自定义时使用 |
100 |
通常在普通 YAML Filter 后、生成目标 URL 前 |
10100 |
生成目标 URL 后、负载均衡前 |
10200 |
负载均衡后、Netty 转发前 |
| 不设置 | 接近最低优先级,即 Integer.MAX_VALUE |
通常统一规定:
text
TraceId:-200
鉴权:-100
普通全局处理:0
不要随意与内置 Filter 使用相同的 order。
Route Filter 只属于某一条 Route;Default Filter 相当于统一给所有 Route 添加 Filter;GlobalFilter 使用 Java 编写全局过滤逻辑。
Route 的order决定先匹配哪条 Route;Filter 的order决定选中 Route 后过滤器的执行顺序,两者不要混淆。
5 Gateway 为什么基于 WebFlux
Spring Cloud Gateway Server WebFlux 基于 Spring WebFlux、Project Reactor 和 Reactor Netty,采用事件驱动、异步非阻塞的编程模型,并依赖 Netty 运行时,不能按照传统 Servlet WAR 的方式运行。
传统阻塞模型中,线程调用数据库、Redis 或远程服务后,可能一直等待 I/O 返回;WebFlux 在等待非阻塞 I/O 时可以让事件循环线程继续处理其他事件,I/O 完成后再执行后续回调。
#mermaid-svg-sEa2aecT6g0IgnBi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sEa2aecT6g0IgnBi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sEa2aecT6g0IgnBi .error-icon{fill:#552222;}#mermaid-svg-sEa2aecT6g0IgnBi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sEa2aecT6g0IgnBi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sEa2aecT6g0IgnBi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sEa2aecT6g0IgnBi .marker.cross{stroke:#333333;}#mermaid-svg-sEa2aecT6g0IgnBi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sEa2aecT6g0IgnBi p{margin:0;}#mermaid-svg-sEa2aecT6g0IgnBi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sEa2aecT6g0IgnBi .cluster-label text{fill:#333;}#mermaid-svg-sEa2aecT6g0IgnBi .cluster-label span{color:#333;}#mermaid-svg-sEa2aecT6g0IgnBi .cluster-label span p{background-color:transparent;}#mermaid-svg-sEa2aecT6g0IgnBi .label text,#mermaid-svg-sEa2aecT6g0IgnBi span{fill:#333;color:#333;}#mermaid-svg-sEa2aecT6g0IgnBi .node rect,#mermaid-svg-sEa2aecT6g0IgnBi .node circle,#mermaid-svg-sEa2aecT6g0IgnBi .node ellipse,#mermaid-svg-sEa2aecT6g0IgnBi .node polygon,#mermaid-svg-sEa2aecT6g0IgnBi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sEa2aecT6g0IgnBi .rough-node .label text,#mermaid-svg-sEa2aecT6g0IgnBi .node .label text,#mermaid-svg-sEa2aecT6g0IgnBi .image-shape .label,#mermaid-svg-sEa2aecT6g0IgnBi .icon-shape .label{text-anchor:middle;}#mermaid-svg-sEa2aecT6g0IgnBi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sEa2aecT6g0IgnBi .rough-node .label,#mermaid-svg-sEa2aecT6g0IgnBi .node .label,#mermaid-svg-sEa2aecT6g0IgnBi .image-shape .label,#mermaid-svg-sEa2aecT6g0IgnBi .icon-shape .label{text-align:center;}#mermaid-svg-sEa2aecT6g0IgnBi .node.clickable{cursor:pointer;}#mermaid-svg-sEa2aecT6g0IgnBi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sEa2aecT6g0IgnBi .arrowheadPath{fill:#333333;}#mermaid-svg-sEa2aecT6g0IgnBi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sEa2aecT6g0IgnBi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sEa2aecT6g0IgnBi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sEa2aecT6g0IgnBi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sEa2aecT6g0IgnBi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sEa2aecT6g0IgnBi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sEa2aecT6g0IgnBi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sEa2aecT6g0IgnBi .cluster text{fill:#333;}#mermaid-svg-sEa2aecT6g0IgnBi .cluster span{color:#333;}#mermaid-svg-sEa2aecT6g0IgnBi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sEa2aecT6g0IgnBi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sEa2aecT6g0IgnBi rect.text{fill:none;stroke-width:0;}#mermaid-svg-sEa2aecT6g0IgnBi .icon-shape,#mermaid-svg-sEa2aecT6g0IgnBi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sEa2aecT6g0IgnBi .icon-shape p,#mermaid-svg-sEa2aecT6g0IgnBi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sEa2aecT6g0IgnBi .icon-shape .label rect,#mermaid-svg-sEa2aecT6g0IgnBi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sEa2aecT6g0IgnBi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sEa2aecT6g0IgnBi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sEa2aecT6g0IgnBi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
少量事件循环线程
处理大量网络连接
是否等待非阻塞I/O
线程处理其他事件
I/O完成后执行回调
继续处理当前请求
Gateway 的主要工作是接收请求、调用下游服务和写回响应,属于典型的高并发网络 I/O 场景,因此适合使用非阻塞模型。
这一块暂时不去了解,知道为什么用 WebFlux 就可以
下面这些操作不能直接放在 Netty 事件循环线程中:
java
Thread.sleep(1000);
java
String result =
restTemplate.getForObject(url, String.class);
java
Object result = future.get();
java
Object result =
jdbcTemplate.queryForObject(sql, type);
Gateway 中应优先使用:
WebClient- Reactive Redis
- R2DBC
- 返回
Mono或Flux的异步接口
确实无法避免阻塞调用时,可以将其切换到专用线程池,但这只是兼容手段,不应该在 Gateway 中大量执行数据库查询或复杂阻塞业务。
WebFlux 不一定让单个请求执行得更快,它的主要价值是减少高并发 I/O 场景中的线程阻塞和线程切换。
6 Gateway 完整请求流程
#mermaid-svg-BJqiTkJtasWq1Tb4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BJqiTkJtasWq1Tb4 .error-icon{fill:#552222;}#mermaid-svg-BJqiTkJtasWq1Tb4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BJqiTkJtasWq1Tb4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .marker.cross{stroke:#333333;}#mermaid-svg-BJqiTkJtasWq1Tb4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BJqiTkJtasWq1Tb4 p{margin:0;}#mermaid-svg-BJqiTkJtasWq1Tb4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster-label text{fill:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster-label span{color:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster-label span p{background-color:transparent;}#mermaid-svg-BJqiTkJtasWq1Tb4 .label text,#mermaid-svg-BJqiTkJtasWq1Tb4 span{fill:#333;color:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .node rect,#mermaid-svg-BJqiTkJtasWq1Tb4 .node circle,#mermaid-svg-BJqiTkJtasWq1Tb4 .node ellipse,#mermaid-svg-BJqiTkJtasWq1Tb4 .node polygon,#mermaid-svg-BJqiTkJtasWq1Tb4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .rough-node .label text,#mermaid-svg-BJqiTkJtasWq1Tb4 .node .label text,#mermaid-svg-BJqiTkJtasWq1Tb4 .image-shape .label,#mermaid-svg-BJqiTkJtasWq1Tb4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-BJqiTkJtasWq1Tb4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .rough-node .label,#mermaid-svg-BJqiTkJtasWq1Tb4 .node .label,#mermaid-svg-BJqiTkJtasWq1Tb4 .image-shape .label,#mermaid-svg-BJqiTkJtasWq1Tb4 .icon-shape .label{text-align:center;}#mermaid-svg-BJqiTkJtasWq1Tb4 .node.clickable{cursor:pointer;}#mermaid-svg-BJqiTkJtasWq1Tb4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .arrowheadPath{fill:#333333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BJqiTkJtasWq1Tb4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BJqiTkJtasWq1Tb4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BJqiTkJtasWq1Tb4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster text{fill:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 .cluster span{color:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BJqiTkJtasWq1Tb4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BJqiTkJtasWq1Tb4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-BJqiTkJtasWq1Tb4 .icon-shape,#mermaid-svg-BJqiTkJtasWq1Tb4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BJqiTkJtasWq1Tb4 .icon-shape p,#mermaid-svg-BJqiTkJtasWq1Tb4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BJqiTkJtasWq1Tb4 .icon-shape .label rect,#mermaid-svg-BJqiTkJtasWq1Tb4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BJqiTkJtasWq1Tb4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BJqiTkJtasWq1Tb4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BJqiTkJtasWq1Tb4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
客户端请求
RoutePredicateHandlerMapping
是否匹配Route
返回404或交给其他Handler
FilteringWebHandler
合并GlobalFilter和GatewayFilter
按照Filter的order排序
执行Pre逻辑
RouteToRequestUrlFilter构造目标地址
是否为lb协议
ReactiveLoadBalancerClientFilter选择实例
直接使用目标地址
NettyRoutingFilter调用下游
下游返回响应
逆序执行Post逻辑
NettyWriteResponseFilter写回响应
官方描述的高层流程是:Handler Mapping 判断请求是否匹配 Route,匹配后交给 Gateway Web Handler,由其执行该请求对应的 Filter Chain;所有 Pre 逻辑执行后代理请求到下游,响应返回后再执行 Post 逻辑。
6.1 RoutePredicateHandlerMapping 匹配 Route
yaml
spring:
cloud:
gateway:
routes:
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
- Method=GET
GET /api/orders/10001 同时满足 Path 和 Method,因此匹配该 Route;POST /api/orders 因为 Method 不匹配,不会进入该 Route。
6.2 FilteringWebHandler 构造 Filter Chain
匹配 Route 后,Gateway 合并 GlobalFilter 和当前 Route 的 GatewayFilter,根据 Filter 的 order 排序并依次执行。
6.3 RouteToRequestUrlFilter 构造目标地址
原始请求为 http://gateway:8080/api/orders/10001,Route URI 为 lb://order-service,Gateway 将目标地址构造成 lb://order-service/api/orders/10001。
6.4 ReactiveLoadBalancerClientFilter 选择实例
如果目标 URI 使用 lb://,ReactiveLoadBalancerClientFilter 会调用 Spring Cloud ReactorLoadBalancer,将服务名解析成真实主机和端口。
例如注册中心返回:
192.168.1.10:8081192.168.1.11:8081192.168.1.12:8081
LoadBalancer 选择一个实例后,目标地址变成 http://192.168.1.11:8081/api/orders/10001。
lb://不是实际网络传输协议,而是表示该 URI 需要经过服务发现和负载均衡。
6.5 NettyRoutingFilter 转发请求
NettyRoutingFilter 使用 Reactor Netty 的 HttpClient 请求下游服务。
6.6 NettyWriteResponseFilter 写回响应
下游响应经过 Post Filter 后,由 NettyWriteResponseFilter 写回客户端。
7 Gateway 登录认证
微服务 Redis Gateway 客户端 微服务 Redis Gateway 客户端 #mermaid-svg-o3TAaBomn6sfOAhO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o3TAaBomn6sfOAhO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o3TAaBomn6sfOAhO .error-icon{fill:#552222;}#mermaid-svg-o3TAaBomn6sfOAhO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o3TAaBomn6sfOAhO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o3TAaBomn6sfOAhO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o3TAaBomn6sfOAhO .marker.cross{stroke:#333333;}#mermaid-svg-o3TAaBomn6sfOAhO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o3TAaBomn6sfOAhO p{margin:0;}#mermaid-svg-o3TAaBomn6sfOAhO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3TAaBomn6sfOAhO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-o3TAaBomn6sfOAhO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o3TAaBomn6sfOAhO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-o3TAaBomn6sfOAhO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-o3TAaBomn6sfOAhO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-o3TAaBomn6sfOAhO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-o3TAaBomn6sfOAhO .sequenceNumber{fill:white;}#mermaid-svg-o3TAaBomn6sfOAhO #sequencenumber{fill:#333;}#mermaid-svg-o3TAaBomn6sfOAhO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-o3TAaBomn6sfOAhO .messageText{fill:#333;stroke:none;}#mermaid-svg-o3TAaBomn6sfOAhO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3TAaBomn6sfOAhO .labelText,#mermaid-svg-o3TAaBomn6sfOAhO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-o3TAaBomn6sfOAhO .loopText,#mermaid-svg-o3TAaBomn6sfOAhO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-o3TAaBomn6sfOAhO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o3TAaBomn6sfOAhO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-o3TAaBomn6sfOAhO .noteText,#mermaid-svg-o3TAaBomn6sfOAhO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-o3TAaBomn6sfOAhO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3TAaBomn6sfOAhO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3TAaBomn6sfOAhO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3TAaBomn6sfOAhO .actorPopupMenu{position:absolute;}#mermaid-svg-o3TAaBomn6sfOAhO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-o3TAaBomn6sfOAhO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3TAaBomn6sfOAhO .actor-man circle,#mermaid-svg-o3TAaBomn6sfOAhO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-o3TAaBomn6sfOAhO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求携带Token判断是否命中白名单提取Token查询登录状态返回用户信息删除客户端身份Header写入认证后的内部身份信息转发请求执行业务权限校验返回响应返回结果
7.1 为什么在 Gateway 校验登录
如果用户、订单、商品和文件服务都单独实现 Token 校验,会产生大量重复代码;统一放到 Gateway 后,可以集中管理:
- 白名单。
- Token 读取规则。
- 登录失效规则。
- 黑名单。
- 通用接口权限。
- 统一未登录响应。
7.2 白名单接口
通常包括:
- 登录和注册。
- 短信验证码。
- 商品公开查询。
- 健康检查。
- Swagger 文档。
#mermaid-svg-nJXANRMa9XxlwJ1O{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nJXANRMa9XxlwJ1O .error-icon{fill:#552222;}#mermaid-svg-nJXANRMa9XxlwJ1O .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nJXANRMa9XxlwJ1O .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nJXANRMa9XxlwJ1O .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nJXANRMa9XxlwJ1O .marker.cross{stroke:#333333;}#mermaid-svg-nJXANRMa9XxlwJ1O svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nJXANRMa9XxlwJ1O p{margin:0;}#mermaid-svg-nJXANRMa9XxlwJ1O .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster-label text{fill:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster-label span{color:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster-label span p{background-color:transparent;}#mermaid-svg-nJXANRMa9XxlwJ1O .label text,#mermaid-svg-nJXANRMa9XxlwJ1O span{fill:#333;color:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O .node rect,#mermaid-svg-nJXANRMa9XxlwJ1O .node circle,#mermaid-svg-nJXANRMa9XxlwJ1O .node ellipse,#mermaid-svg-nJXANRMa9XxlwJ1O .node polygon,#mermaid-svg-nJXANRMa9XxlwJ1O .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nJXANRMa9XxlwJ1O .rough-node .label text,#mermaid-svg-nJXANRMa9XxlwJ1O .node .label text,#mermaid-svg-nJXANRMa9XxlwJ1O .image-shape .label,#mermaid-svg-nJXANRMa9XxlwJ1O .icon-shape .label{text-anchor:middle;}#mermaid-svg-nJXANRMa9XxlwJ1O .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nJXANRMa9XxlwJ1O .rough-node .label,#mermaid-svg-nJXANRMa9XxlwJ1O .node .label,#mermaid-svg-nJXANRMa9XxlwJ1O .image-shape .label,#mermaid-svg-nJXANRMa9XxlwJ1O .icon-shape .label{text-align:center;}#mermaid-svg-nJXANRMa9XxlwJ1O .node.clickable{cursor:pointer;}#mermaid-svg-nJXANRMa9XxlwJ1O .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nJXANRMa9XxlwJ1O .arrowheadPath{fill:#333333;}#mermaid-svg-nJXANRMa9XxlwJ1O .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nJXANRMa9XxlwJ1O .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nJXANRMa9XxlwJ1O .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nJXANRMa9XxlwJ1O .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nJXANRMa9XxlwJ1O .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nJXANRMa9XxlwJ1O .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster text{fill:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O .cluster span{color:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nJXANRMa9XxlwJ1O .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nJXANRMa9XxlwJ1O rect.text{fill:none;stroke-width:0;}#mermaid-svg-nJXANRMa9XxlwJ1O .icon-shape,#mermaid-svg-nJXANRMa9XxlwJ1O .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nJXANRMa9XxlwJ1O .icon-shape p,#mermaid-svg-nJXANRMa9XxlwJ1O .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nJXANRMa9XxlwJ1O .icon-shape .label rect,#mermaid-svg-nJXANRMa9XxlwJ1O .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nJXANRMa9XxlwJ1O .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nJXANRMa9XxlwJ1O .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nJXANRMa9XxlwJ1O :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
请求进入Gateway
是否命中白名单
直接放行
校验Token
Token是否合法
返回401
继续执行Filter Chain
7.3 使用 Sa-Token 鉴权
java
@Configuration
public class SaTokenGatewayConfig {
@Bean
public SaReactorFilter saReactorFilter() {
return new SaReactorFilter()
.addInclude("/**")
.addExclude(
"/api/auth/login",
"/api/auth/register",
"/api/auth/sms-code",
"/api/public/**",
"/actuator/health"
)
.setAuth(object -> StpUtil.checkLogin());
}
}
如果登录状态存储在 Redis 中,多个 Gateway 实例可以共享登录状态,不需要使用粘性会话。
8 Gateway 如何向微服务传递用户信息
Gateway 校验 Token 后,可以将用户 ID 写入请求头:
X-User-Id: 10001
微服务读取:
java
@GetMapping("/profile")
public UserProfileResponse getProfile(
@RequestHeader("X-User-Id") Long userId) {
return userService.getProfile(userId);
}
客户端也可以主动提交 X-User-Id,因此 Gateway 不能直接透传这些身份 Header,而应该先删除,再根据认证结果重新写入。
java
ServerHttpRequest request = exchange.getRequest()
.mutate()
.headers(headers -> {
headers.remove("X-User-Id");
headers.remove("X-User-Role");
headers.remove("X-Tenant-Id");
headers.set("X-User-Id", loginUserId);
headers.set("X-User-Role", role);
headers.set("X-Tenant-Id", tenantId);
})
.build();
#mermaid-svg-3ghtefEgeqbyHHcU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3ghtefEgeqbyHHcU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3ghtefEgeqbyHHcU .error-icon{fill:#552222;}#mermaid-svg-3ghtefEgeqbyHHcU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3ghtefEgeqbyHHcU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3ghtefEgeqbyHHcU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3ghtefEgeqbyHHcU .marker.cross{stroke:#333333;}#mermaid-svg-3ghtefEgeqbyHHcU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3ghtefEgeqbyHHcU p{margin:0;}#mermaid-svg-3ghtefEgeqbyHHcU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3ghtefEgeqbyHHcU .cluster-label text{fill:#333;}#mermaid-svg-3ghtefEgeqbyHHcU .cluster-label span{color:#333;}#mermaid-svg-3ghtefEgeqbyHHcU .cluster-label span p{background-color:transparent;}#mermaid-svg-3ghtefEgeqbyHHcU .label text,#mermaid-svg-3ghtefEgeqbyHHcU span{fill:#333;color:#333;}#mermaid-svg-3ghtefEgeqbyHHcU .node rect,#mermaid-svg-3ghtefEgeqbyHHcU .node circle,#mermaid-svg-3ghtefEgeqbyHHcU .node ellipse,#mermaid-svg-3ghtefEgeqbyHHcU .node polygon,#mermaid-svg-3ghtefEgeqbyHHcU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3ghtefEgeqbyHHcU .rough-node .label text,#mermaid-svg-3ghtefEgeqbyHHcU .node .label text,#mermaid-svg-3ghtefEgeqbyHHcU .image-shape .label,#mermaid-svg-3ghtefEgeqbyHHcU .icon-shape .label{text-anchor:middle;}#mermaid-svg-3ghtefEgeqbyHHcU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3ghtefEgeqbyHHcU .rough-node .label,#mermaid-svg-3ghtefEgeqbyHHcU .node .label,#mermaid-svg-3ghtefEgeqbyHHcU .image-shape .label,#mermaid-svg-3ghtefEgeqbyHHcU .icon-shape .label{text-align:center;}#mermaid-svg-3ghtefEgeqbyHHcU .node.clickable{cursor:pointer;}#mermaid-svg-3ghtefEgeqbyHHcU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3ghtefEgeqbyHHcU .arrowheadPath{fill:#333333;}#mermaid-svg-3ghtefEgeqbyHHcU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3ghtefEgeqbyHHcU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3ghtefEgeqbyHHcU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3ghtefEgeqbyHHcU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3ghtefEgeqbyHHcU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3ghtefEgeqbyHHcU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3ghtefEgeqbyHHcU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3ghtefEgeqbyHHcU .cluster text{fill:#333;}#mermaid-svg-3ghtefEgeqbyHHcU .cluster span{color:#333;}#mermaid-svg-3ghtefEgeqbyHHcU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3ghtefEgeqbyHHcU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3ghtefEgeqbyHHcU rect.text{fill:none;stroke-width:0;}#mermaid-svg-3ghtefEgeqbyHHcU .icon-shape,#mermaid-svg-3ghtefEgeqbyHHcU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3ghtefEgeqbyHHcU .icon-shape p,#mermaid-svg-3ghtefEgeqbyHHcU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3ghtefEgeqbyHHcU .icon-shape .label rect,#mermaid-svg-3ghtefEgeqbyHHcU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3ghtefEgeqbyHHcU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3ghtefEgeqbyHHcU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3ghtefEgeqbyHHcU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端提交身份Header
Gateway无条件删除
根据Token获取真实登录信息
Gateway重新写入内部身份Header
微服务读取
但是,仅删除并重写 Header 仍然不能保证安全;下游只有在满足以下条件时才能信任这些 Header:
- 微服务没有暴露公网端口。
- 安全组或网络策略只允许 Gateway 访问微服务。
- 客户端无法绕过 Gateway。
- 高安全场景增加 HMAC、内部 JWT 或 mTLS。
#mermaid-svg-7NYqPxlUrsChWaqF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7NYqPxlUrsChWaqF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7NYqPxlUrsChWaqF .error-icon{fill:#552222;}#mermaid-svg-7NYqPxlUrsChWaqF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7NYqPxlUrsChWaqF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7NYqPxlUrsChWaqF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7NYqPxlUrsChWaqF .marker.cross{stroke:#333333;}#mermaid-svg-7NYqPxlUrsChWaqF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7NYqPxlUrsChWaqF p{margin:0;}#mermaid-svg-7NYqPxlUrsChWaqF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7NYqPxlUrsChWaqF .cluster-label text{fill:#333;}#mermaid-svg-7NYqPxlUrsChWaqF .cluster-label span{color:#333;}#mermaid-svg-7NYqPxlUrsChWaqF .cluster-label span p{background-color:transparent;}#mermaid-svg-7NYqPxlUrsChWaqF .label text,#mermaid-svg-7NYqPxlUrsChWaqF span{fill:#333;color:#333;}#mermaid-svg-7NYqPxlUrsChWaqF .node rect,#mermaid-svg-7NYqPxlUrsChWaqF .node circle,#mermaid-svg-7NYqPxlUrsChWaqF .node ellipse,#mermaid-svg-7NYqPxlUrsChWaqF .node polygon,#mermaid-svg-7NYqPxlUrsChWaqF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7NYqPxlUrsChWaqF .rough-node .label text,#mermaid-svg-7NYqPxlUrsChWaqF .node .label text,#mermaid-svg-7NYqPxlUrsChWaqF .image-shape .label,#mermaid-svg-7NYqPxlUrsChWaqF .icon-shape .label{text-anchor:middle;}#mermaid-svg-7NYqPxlUrsChWaqF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7NYqPxlUrsChWaqF .rough-node .label,#mermaid-svg-7NYqPxlUrsChWaqF .node .label,#mermaid-svg-7NYqPxlUrsChWaqF .image-shape .label,#mermaid-svg-7NYqPxlUrsChWaqF .icon-shape .label{text-align:center;}#mermaid-svg-7NYqPxlUrsChWaqF .node.clickable{cursor:pointer;}#mermaid-svg-7NYqPxlUrsChWaqF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7NYqPxlUrsChWaqF .arrowheadPath{fill:#333333;}#mermaid-svg-7NYqPxlUrsChWaqF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7NYqPxlUrsChWaqF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7NYqPxlUrsChWaqF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7NYqPxlUrsChWaqF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7NYqPxlUrsChWaqF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7NYqPxlUrsChWaqF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7NYqPxlUrsChWaqF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7NYqPxlUrsChWaqF .cluster text{fill:#333;}#mermaid-svg-7NYqPxlUrsChWaqF .cluster span{color:#333;}#mermaid-svg-7NYqPxlUrsChWaqF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7NYqPxlUrsChWaqF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7NYqPxlUrsChWaqF rect.text{fill:none;stroke-width:0;}#mermaid-svg-7NYqPxlUrsChWaqF .icon-shape,#mermaid-svg-7NYqPxlUrsChWaqF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7NYqPxlUrsChWaqF .icon-shape p,#mermaid-svg-7NYqPxlUrsChWaqF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7NYqPxlUrsChWaqF .icon-shape .label rect,#mermaid-svg-7NYqPxlUrsChWaqF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7NYqPxlUrsChWaqF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7NYqPxlUrsChWaqF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7NYqPxlUrsChWaqF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
Gateway删除身份Header
根据Token重新生成
网络策略只允许Gateway访问微服务
微服务信任内部身份信息
Header 只是信息载体,并不天然可信,可信性来自认证过程、网络隔离和防伪机制。
9 Gateway 鉴权和微服务鉴权如何分工
| Gateway | 微服务 |
|---|---|
| 用户是否登录 | 资源是否属于当前用户 |
| Token 是否有效 | 当前用户是否有数据权限 |
| 通用角色权限 | 当前订单是否允许取消 |
| 黑名单 | 当前商品是否属于当前商家 |
| 通用接口权限 | 复杂业务状态和规则 |
| 通用限流 | 业务级风控 |
例如取消订单时,Gateway 可以判断用户是否登录、是否拥有 order:cancel 权限;order-service 仍然要判断:
- 订单是否属于当前用户。
- 订单是否已经支付。
- 订单是否已经发货。
- 当前订单状态是否允许取消。
Gateway 做粗粒度、通用鉴权,微服务做细粒度、业务鉴权。
10 Gateway 限流
限流的目标是在请求到达微服务之前控制流量,避免大量请求继续压垮微服务、数据库和 Redis。
#mermaid-svg-56vuxwes03R1QwI3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-56vuxwes03R1QwI3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-56vuxwes03R1QwI3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-56vuxwes03R1QwI3 .error-icon{fill:#552222;}#mermaid-svg-56vuxwes03R1QwI3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-56vuxwes03R1QwI3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-56vuxwes03R1QwI3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-56vuxwes03R1QwI3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-56vuxwes03R1QwI3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-56vuxwes03R1QwI3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-56vuxwes03R1QwI3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-56vuxwes03R1QwI3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-56vuxwes03R1QwI3 .marker.cross{stroke:#333333;}#mermaid-svg-56vuxwes03R1QwI3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-56vuxwes03R1QwI3 p{margin:0;}#mermaid-svg-56vuxwes03R1QwI3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-56vuxwes03R1QwI3 .cluster-label text{fill:#333;}#mermaid-svg-56vuxwes03R1QwI3 .cluster-label span{color:#333;}#mermaid-svg-56vuxwes03R1QwI3 .cluster-label span p{background-color:transparent;}#mermaid-svg-56vuxwes03R1QwI3 .label text,#mermaid-svg-56vuxwes03R1QwI3 span{fill:#333;color:#333;}#mermaid-svg-56vuxwes03R1QwI3 .node rect,#mermaid-svg-56vuxwes03R1QwI3 .node circle,#mermaid-svg-56vuxwes03R1QwI3 .node ellipse,#mermaid-svg-56vuxwes03R1QwI3 .node polygon,#mermaid-svg-56vuxwes03R1QwI3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-56vuxwes03R1QwI3 .rough-node .label text,#mermaid-svg-56vuxwes03R1QwI3 .node .label text,#mermaid-svg-56vuxwes03R1QwI3 .image-shape .label,#mermaid-svg-56vuxwes03R1QwI3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-56vuxwes03R1QwI3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-56vuxwes03R1QwI3 .rough-node .label,#mermaid-svg-56vuxwes03R1QwI3 .node .label,#mermaid-svg-56vuxwes03R1QwI3 .image-shape .label,#mermaid-svg-56vuxwes03R1QwI3 .icon-shape .label{text-align:center;}#mermaid-svg-56vuxwes03R1QwI3 .node.clickable{cursor:pointer;}#mermaid-svg-56vuxwes03R1QwI3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-56vuxwes03R1QwI3 .arrowheadPath{fill:#333333;}#mermaid-svg-56vuxwes03R1QwI3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-56vuxwes03R1QwI3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-56vuxwes03R1QwI3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-56vuxwes03R1QwI3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-56vuxwes03R1QwI3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-56vuxwes03R1QwI3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-56vuxwes03R1QwI3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-56vuxwes03R1QwI3 .cluster text{fill:#333;}#mermaid-svg-56vuxwes03R1QwI3 .cluster span{color:#333;}#mermaid-svg-56vuxwes03R1QwI3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-56vuxwes03R1QwI3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-56vuxwes03R1QwI3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-56vuxwes03R1QwI3 .icon-shape,#mermaid-svg-56vuxwes03R1QwI3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-56vuxwes03R1QwI3 .icon-shape p,#mermaid-svg-56vuxwes03R1QwI3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-56vuxwes03R1QwI3 .icon-shape .label rect,#mermaid-svg-56vuxwes03R1QwI3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-56vuxwes03R1QwI3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-56vuxwes03R1QwI3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-56vuxwes03R1QwI3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
大量请求
Gateway
是否超过限流阈值
返回429
转发到微服务
10.1 常见限流维度
| 维度 | 示例 |
|---|---|
| IP | 每个 IP 每秒最多 10 次 |
| 用户 | 每个用户每秒最多 20 次 |
| 接口 | 登录接口每秒最多 1000 次 |
| 租户 | 每个租户每秒最多 500 次 |
| Route | 某条 Route 每秒最多 3000 次 |
| API 分组 | 支付相关 API 共享一组限流规则 |
| 全局 | 整个系统每秒最多 1 万次 |
10.2 RequestRateLimiter
RequestRateLimiter 是路由过滤器,它通过一个 RateLimiter 实现判断请求是否允许继续执行;默认拒绝状态码是 429 Too Many Requests。当前官方实现包括 RedisRateLimiter、Bucket4j RateLimiter,也支持自定义 RateLimiter。
10.3 RedisRateLimiter
RedisRateLimiter 使用令牌桶算法:
#mermaid-svg-oGeQMMGn2OoioAlF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oGeQMMGn2OoioAlF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oGeQMMGn2OoioAlF .error-icon{fill:#552222;}#mermaid-svg-oGeQMMGn2OoioAlF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oGeQMMGn2OoioAlF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oGeQMMGn2OoioAlF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oGeQMMGn2OoioAlF .marker.cross{stroke:#333333;}#mermaid-svg-oGeQMMGn2OoioAlF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oGeQMMGn2OoioAlF p{margin:0;}#mermaid-svg-oGeQMMGn2OoioAlF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oGeQMMGn2OoioAlF .cluster-label text{fill:#333;}#mermaid-svg-oGeQMMGn2OoioAlF .cluster-label span{color:#333;}#mermaid-svg-oGeQMMGn2OoioAlF .cluster-label span p{background-color:transparent;}#mermaid-svg-oGeQMMGn2OoioAlF .label text,#mermaid-svg-oGeQMMGn2OoioAlF span{fill:#333;color:#333;}#mermaid-svg-oGeQMMGn2OoioAlF .node rect,#mermaid-svg-oGeQMMGn2OoioAlF .node circle,#mermaid-svg-oGeQMMGn2OoioAlF .node ellipse,#mermaid-svg-oGeQMMGn2OoioAlF .node polygon,#mermaid-svg-oGeQMMGn2OoioAlF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oGeQMMGn2OoioAlF .rough-node .label text,#mermaid-svg-oGeQMMGn2OoioAlF .node .label text,#mermaid-svg-oGeQMMGn2OoioAlF .image-shape .label,#mermaid-svg-oGeQMMGn2OoioAlF .icon-shape .label{text-anchor:middle;}#mermaid-svg-oGeQMMGn2OoioAlF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oGeQMMGn2OoioAlF .rough-node .label,#mermaid-svg-oGeQMMGn2OoioAlF .node .label,#mermaid-svg-oGeQMMGn2OoioAlF .image-shape .label,#mermaid-svg-oGeQMMGn2OoioAlF .icon-shape .label{text-align:center;}#mermaid-svg-oGeQMMGn2OoioAlF .node.clickable{cursor:pointer;}#mermaid-svg-oGeQMMGn2OoioAlF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oGeQMMGn2OoioAlF .arrowheadPath{fill:#333333;}#mermaid-svg-oGeQMMGn2OoioAlF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oGeQMMGn2OoioAlF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oGeQMMGn2OoioAlF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGeQMMGn2OoioAlF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oGeQMMGn2OoioAlF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGeQMMGn2OoioAlF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oGeQMMGn2OoioAlF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oGeQMMGn2OoioAlF .cluster text{fill:#333;}#mermaid-svg-oGeQMMGn2OoioAlF .cluster span{color:#333;}#mermaid-svg-oGeQMMGn2OoioAlF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oGeQMMGn2OoioAlF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oGeQMMGn2OoioAlF rect.text{fill:none;stroke-width:0;}#mermaid-svg-oGeQMMGn2OoioAlF .icon-shape,#mermaid-svg-oGeQMMGn2OoioAlF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGeQMMGn2OoioAlF .icon-shape p,#mermaid-svg-oGeQMMGn2OoioAlF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oGeQMMGn2OoioAlF .icon-shape .label rect,#mermaid-svg-oGeQMMGn2OoioAlF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGeQMMGn2OoioAlF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oGeQMMGn2OoioAlF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oGeQMMGn2OoioAlF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
按照固定速率补充令牌
令牌桶
请求到达
是否有足够令牌
扣减令牌并放行
返回429
yaml
spring:
cloud:
gateway:
routes:
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
# 使用 Spring 容器中名为 userKeyResolver 的 Bean
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
| 参数 | 准确含义 |
|---|---|
replenishRate |
每秒补充多少令牌 |
burstCapacity |
令牌桶最多保存多少令牌 |
requestedTokens |
每个请求需要消耗多少令牌 |
当 requestedTokens=1 时,上述配置表示稳定情况下每秒补充 10 个请求所需的令牌,桶最多保存 20 个令牌,因此可以允许一定程度的瞬时突发流量。
只有
requestedTokens=1时,replenishRate=10才能简单理解为每秒约 10 个请求;如果每个请求消耗 2 个令牌,稳定请求速率约为每秒 5 个。
10.4 RedisRateLimiter 为什么使用 Lua
分布式令牌桶需要完成读取剩余令牌、计算补充数量、判断是否允许、扣减令牌和更新时间等操作,如果拆成多条 Redis 命令,多实例并发时会产生竞态条件。
RedisRateLimiter 使用 Redis Lua 脚本一次完成这些操作,Redis 脚本执行期间具有原子性,避免读写之间被其他命令插入。当前源码中确实通过 Lua 同时维护令牌数和刷新时间。
#mermaid-svg-q9WHDRGP2ftwdvbu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q9WHDRGP2ftwdvbu .error-icon{fill:#552222;}#mermaid-svg-q9WHDRGP2ftwdvbu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q9WHDRGP2ftwdvbu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q9WHDRGP2ftwdvbu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q9WHDRGP2ftwdvbu .marker.cross{stroke:#333333;}#mermaid-svg-q9WHDRGP2ftwdvbu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q9WHDRGP2ftwdvbu p{margin:0;}#mermaid-svg-q9WHDRGP2ftwdvbu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster-label text{fill:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster-label span{color:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster-label span p{background-color:transparent;}#mermaid-svg-q9WHDRGP2ftwdvbu .label text,#mermaid-svg-q9WHDRGP2ftwdvbu span{fill:#333;color:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu .node rect,#mermaid-svg-q9WHDRGP2ftwdvbu .node circle,#mermaid-svg-q9WHDRGP2ftwdvbu .node ellipse,#mermaid-svg-q9WHDRGP2ftwdvbu .node polygon,#mermaid-svg-q9WHDRGP2ftwdvbu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q9WHDRGP2ftwdvbu .rough-node .label text,#mermaid-svg-q9WHDRGP2ftwdvbu .node .label text,#mermaid-svg-q9WHDRGP2ftwdvbu .image-shape .label,#mermaid-svg-q9WHDRGP2ftwdvbu .icon-shape .label{text-anchor:middle;}#mermaid-svg-q9WHDRGP2ftwdvbu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q9WHDRGP2ftwdvbu .rough-node .label,#mermaid-svg-q9WHDRGP2ftwdvbu .node .label,#mermaid-svg-q9WHDRGP2ftwdvbu .image-shape .label,#mermaid-svg-q9WHDRGP2ftwdvbu .icon-shape .label{text-align:center;}#mermaid-svg-q9WHDRGP2ftwdvbu .node.clickable{cursor:pointer;}#mermaid-svg-q9WHDRGP2ftwdvbu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q9WHDRGP2ftwdvbu .arrowheadPath{fill:#333333;}#mermaid-svg-q9WHDRGP2ftwdvbu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q9WHDRGP2ftwdvbu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q9WHDRGP2ftwdvbu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q9WHDRGP2ftwdvbu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q9WHDRGP2ftwdvbu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q9WHDRGP2ftwdvbu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster text{fill:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu .cluster span{color:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q9WHDRGP2ftwdvbu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q9WHDRGP2ftwdvbu rect.text{fill:none;stroke-width:0;}#mermaid-svg-q9WHDRGP2ftwdvbu .icon-shape,#mermaid-svg-q9WHDRGP2ftwdvbu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q9WHDRGP2ftwdvbu .icon-shape p,#mermaid-svg-q9WHDRGP2ftwdvbu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q9WHDRGP2ftwdvbu .icon-shape .label rect,#mermaid-svg-q9WHDRGP2ftwdvbu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q9WHDRGP2ftwdvbu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q9WHDRGP2ftwdvbu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q9WHDRGP2ftwdvbu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Gateway实例1
Redis Lua脚本
Gateway实例2
计算补充令牌
判断是否允许
原子更新令牌和时间
需要注意:RedisRateLimiter 当前源码在 Redis 调用异常时采取 放行请求的 fail-open 策略,避免 Redis 故障直接阻断全部业务,因此项目必须监控限流脚本异常,并根据业务安全等级评估是否需要额外的本地限流或故障策略。
10.5 KeyResolver
KeyResolver 决定按照什么 Key 限流。
按用户 ID 限流:
java
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id");
if (userId == null || userId.isBlank()) {
return Mono.empty();
}
return Mono.just(userId);
};
}
按用户和接口组合限流:
java
@Bean
public KeyResolver userApiKeyResolver() {
return exchange -> {
String userId = exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id");
String path = exchange.getRequest()
.getPath()
.value();
if (userId == null || userId.isBlank()) {
return Mono.empty();
}
return Mono.just(userId + ":" + path);
};
}
默认情况下,KeyResolver 无法解析出 Key 时,请求会被拒绝;项目也可以根据自身需求返回 "anonymous",但这意味着所有匿名用户可能共享同一个限流桶。
10.6 RedisRateLimiter 和 Sentinel 如何选择
| RedisRateLimiter | Sentinel Gateway Adapter |
|---|---|
| Gateway 原生支持 | 需要额外集成 Sentinel |
| 适合基础令牌桶限流 | 支持 Route、API 分组等规则 |
| 依赖 Redis Reactive | 依赖 Sentinel 规则管理体系 |
| 配置较简单 | 流量治理能力更丰富 |
| 适合用户、IP、Route 等 Key | 适合统一流控、热点和动态规则 |
RedisRateLimiter 适合基础分布式限流;需要 Route 分组、动态规则管理和更复杂治理能力时,可以考虑 Sentinel,但不要为了简单限流无条件增加系统复杂度。
11 超时、重试、熔断和降级
| 机制 | 解决的问题 |
|---|---|
| 超时 | 下游一直不返回,不能无限等待 |
| 重试 | 临时网络抖动,重新请求可能成功 |
| 熔断 | 下游持续异常,暂时停止调用 |
| 降级 | 无法正常调用时返回兜底结果 |
| 限流 | 进入系统的请求过多 |
| 隔离 | 避免一个服务占满全部资源 |
11.1 超时
- 连接超时:规定多长时间无法建立 TCP 连接就失败。
- 响应超时:规定连接建立后,多长时间没有完成响应就失败。
5.x 配置示例:
yaml
spring:
cloud:
gateway:
server:
webflux:
httpclient:
connect-timeout: 3000
response-timeout: 5s
4.x 常见配置示例:
yaml
spring:
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
超时时间需要结合正常耗时、P99 延迟、用户体验和下游 SLA 设置,不能简单认为越短越好。
11.2 重试
yaml
spring:
cloud:
gateway:
routes:
- id: query-order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: Retry
args:
retries: 2
statuses:
- BAD_GATEWAY # 502
- SERVICE_UNAVAILABLE # 503
- GATEWAY_TIMEOUT # 504
methods:
- GET
backoff: # 退避重试
firstBackoff: 100ms
maxBackoff: 500ms
factor: 2 # 第一次等待 100ms 重试,第二次等待 200ms 重试,之后不重试了
basedOnPreviousValue: false
retries: 2 表示初始请求失败后最多再重试 2 次,因此最坏情况下下游总调用次数为 3 次。
适合自动重试:
- GET 查询。
- 临时连接异常。
- 短暂网关错误。
- 确认具有幂等性的请求。
上面的含义是必须是 GET 请求,并且错误码是 BAD_GATEWAY 或者 SERVICE_UNAVAILABLE 或者 GATEWAY_TIMEOUT 才可以重试。
不应该随意自动重试:
- 创建订单。
- 支付。
- 转账。
- 发券。
- 扣减库存。
写请求需要配合:
- 幂等 Key。
- 业务唯一键。
- 数据库唯一索引。
- 去重记录。
- 业务状态机。
Retry Filter 会重新执行位于它之后的 Filter,因此后续 Filter 必须能够安全地重复执行;如果请求携带 Body,Gateway 还需要缓存请求体,可能产生较大的内存压力。
#mermaid-svg-FhFObhMY0Zht7jxm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FhFObhMY0Zht7jxm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FhFObhMY0Zht7jxm .error-icon{fill:#552222;}#mermaid-svg-FhFObhMY0Zht7jxm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FhFObhMY0Zht7jxm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FhFObhMY0Zht7jxm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FhFObhMY0Zht7jxm .marker.cross{stroke:#333333;}#mermaid-svg-FhFObhMY0Zht7jxm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FhFObhMY0Zht7jxm p{margin:0;}#mermaid-svg-FhFObhMY0Zht7jxm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FhFObhMY0Zht7jxm .cluster-label text{fill:#333;}#mermaid-svg-FhFObhMY0Zht7jxm .cluster-label span{color:#333;}#mermaid-svg-FhFObhMY0Zht7jxm .cluster-label span p{background-color:transparent;}#mermaid-svg-FhFObhMY0Zht7jxm .label text,#mermaid-svg-FhFObhMY0Zht7jxm span{fill:#333;color:#333;}#mermaid-svg-FhFObhMY0Zht7jxm .node rect,#mermaid-svg-FhFObhMY0Zht7jxm .node circle,#mermaid-svg-FhFObhMY0Zht7jxm .node ellipse,#mermaid-svg-FhFObhMY0Zht7jxm .node polygon,#mermaid-svg-FhFObhMY0Zht7jxm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FhFObhMY0Zht7jxm .rough-node .label text,#mermaid-svg-FhFObhMY0Zht7jxm .node .label text,#mermaid-svg-FhFObhMY0Zht7jxm .image-shape .label,#mermaid-svg-FhFObhMY0Zht7jxm .icon-shape .label{text-anchor:middle;}#mermaid-svg-FhFObhMY0Zht7jxm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FhFObhMY0Zht7jxm .rough-node .label,#mermaid-svg-FhFObhMY0Zht7jxm .node .label,#mermaid-svg-FhFObhMY0Zht7jxm .image-shape .label,#mermaid-svg-FhFObhMY0Zht7jxm .icon-shape .label{text-align:center;}#mermaid-svg-FhFObhMY0Zht7jxm .node.clickable{cursor:pointer;}#mermaid-svg-FhFObhMY0Zht7jxm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FhFObhMY0Zht7jxm .arrowheadPath{fill:#333333;}#mermaid-svg-FhFObhMY0Zht7jxm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FhFObhMY0Zht7jxm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FhFObhMY0Zht7jxm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FhFObhMY0Zht7jxm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FhFObhMY0Zht7jxm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FhFObhMY0Zht7jxm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FhFObhMY0Zht7jxm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FhFObhMY0Zht7jxm .cluster text{fill:#333;}#mermaid-svg-FhFObhMY0Zht7jxm .cluster span{color:#333;}#mermaid-svg-FhFObhMY0Zht7jxm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FhFObhMY0Zht7jxm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FhFObhMY0Zht7jxm rect.text{fill:none;stroke-width:0;}#mermaid-svg-FhFObhMY0Zht7jxm .icon-shape,#mermaid-svg-FhFObhMY0Zht7jxm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FhFObhMY0Zht7jxm .icon-shape p,#mermaid-svg-FhFObhMY0Zht7jxm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FhFObhMY0Zht7jxm .icon-shape .label rect,#mermaid-svg-FhFObhMY0Zht7jxm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FhFObhMY0Zht7jxm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FhFObhMY0Zht7jxm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FhFObhMY0Zht7jxm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
第一次调用
是否成功
返回响应
等待Backoff
第一次重试
是否成功
第二次重试
最终结果
重试会放大下游流量,配置不当可能形成重试风暴,重试次数通常应该很小并配合退避。
11.3 熔断
熔断用于保护调用方和故障服务。当下游服务持续报错或响应过慢时,熔断器会暂时阻止新的请求继续访问该服务,避免大量请求一直等待、重试,最终拖垮整个系统。
例如 Gateway 调用 order-service:
text
Gateway → order-service
如果订单服务已经故障,大量请求仍然继续调用,就会出现:
text
请求不断进入
→ 线程和连接持续等待
→ Gateway 资源被占满
→ 其他正常服务也受到影响
加入熔断器后:
text
连续调用失败
→ 熔断器打开
→ 暂时不再调用 order-service
→ 请求直接快速失败或执行降级逻辑
熔断器主要有三种状态:
#mermaid-svg-Ge8boaHHyJ1to2Oc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ge8boaHHyJ1to2Oc .error-icon{fill:#552222;}#mermaid-svg-Ge8boaHHyJ1to2Oc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ge8boaHHyJ1to2Oc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .marker.cross{stroke:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ge8boaHHyJ1to2Oc p{margin:0;}#mermaid-svg-Ge8boaHHyJ1to2Oc defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-Ge8boaHHyJ1to2Oc g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-Ge8boaHHyJ1to2Oc g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-Ge8boaHHyJ1to2Oc g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Ge8boaHHyJ1to2Oc g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-Ge8boaHHyJ1to2Oc .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Ge8boaHHyJ1to2Oc .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Ge8boaHHyJ1to2Oc .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ge8boaHHyJ1to2Oc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ge8boaHHyJ1to2Oc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ge8boaHHyJ1to2Oc .edgeLabel .label text{fill:#333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .label div .edgeLabel{color:#333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-Ge8boaHHyJ1to2Oc .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-Ge8boaHHyJ1to2Oc .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-Ge8boaHHyJ1to2Oc .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc #statediagram-barbEnd{fill:#333333;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .cluster-label,#mermaid-svg-Ge8boaHHyJ1to2Oc .nodeLabel{color:#131300;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .note-edge{stroke-dasharray:5;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-note text{fill:black;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram-note .nodeLabel{color:black;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagram .edgeLabel{color:red;}#mermaid-svg-Ge8boaHHyJ1to2Oc #dependencyStart,#mermaid-svg-Ge8boaHHyJ1to2Oc #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-Ge8boaHHyJ1to2Oc .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ge8boaHHyJ1to2Oc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败率或慢调用达到阈值
等待时间结束
试探请求成功
试探请求失败
Closed
Open
HalfOpen
Closed:关闭状态
这里的"关闭"表示保险丝没有断开,可以正常调用下游服务。
text
请求 → 熔断器 → order-service
熔断器会统计最近一段时间内的调用结果,例如:
text
最近 10 次请求:
成功 8 次
失败 2 次
失败率 = 20%
如果失败率没有达到配置的阈值,就继续正常调用。
Open:打开状态
当失败率或慢调用比例达到阈值时,熔断器打开。
例如:
text
最近 10 次请求:
成功 4 次
失败 6 次
失败率 = 60%
熔断阈值 = 50%
此时后续请求不会再调用 order-service,而是直接快速失败或执行 fallback:
text
请求 → 熔断器 → 降级结果
这样可以:
- 避免继续冲击已经故障的服务;
- 避免请求长时间等待;
- 保护 Gateway 的线程、连接等资源;
- 给下游服务留下恢复时间。
Half-Open:半开状态
熔断器不能永远处于 Open 状态,否则即使下游恢复,也无法重新访问。
因此等待一段时间后,熔断器会进入 Half-Open 状态,允许少量请求试探下游服务:
text
放行 3 个试探请求
如果这些请求大部分成功:
text
order-service 已恢复
→ 熔断器重新进入 Closed
→ 恢复正常调用
如果试探请求仍然失败:
text
order-service 仍未恢复
→ 熔断器重新进入 Open
→ 继续快速失败
完整过程
#mermaid-svg-1dLJoBoidBfNSG9a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1dLJoBoidBfNSG9a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1dLJoBoidBfNSG9a .error-icon{fill:#552222;}#mermaid-svg-1dLJoBoidBfNSG9a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1dLJoBoidBfNSG9a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1dLJoBoidBfNSG9a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1dLJoBoidBfNSG9a .marker.cross{stroke:#333333;}#mermaid-svg-1dLJoBoidBfNSG9a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1dLJoBoidBfNSG9a p{margin:0;}#mermaid-svg-1dLJoBoidBfNSG9a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1dLJoBoidBfNSG9a .cluster-label text{fill:#333;}#mermaid-svg-1dLJoBoidBfNSG9a .cluster-label span{color:#333;}#mermaid-svg-1dLJoBoidBfNSG9a .cluster-label span p{background-color:transparent;}#mermaid-svg-1dLJoBoidBfNSG9a .label text,#mermaid-svg-1dLJoBoidBfNSG9a span{fill:#333;color:#333;}#mermaid-svg-1dLJoBoidBfNSG9a .node rect,#mermaid-svg-1dLJoBoidBfNSG9a .node circle,#mermaid-svg-1dLJoBoidBfNSG9a .node ellipse,#mermaid-svg-1dLJoBoidBfNSG9a .node polygon,#mermaid-svg-1dLJoBoidBfNSG9a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1dLJoBoidBfNSG9a .rough-node .label text,#mermaid-svg-1dLJoBoidBfNSG9a .node .label text,#mermaid-svg-1dLJoBoidBfNSG9a .image-shape .label,#mermaid-svg-1dLJoBoidBfNSG9a .icon-shape .label{text-anchor:middle;}#mermaid-svg-1dLJoBoidBfNSG9a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1dLJoBoidBfNSG9a .rough-node .label,#mermaid-svg-1dLJoBoidBfNSG9a .node .label,#mermaid-svg-1dLJoBoidBfNSG9a .image-shape .label,#mermaid-svg-1dLJoBoidBfNSG9a .icon-shape .label{text-align:center;}#mermaid-svg-1dLJoBoidBfNSG9a .node.clickable{cursor:pointer;}#mermaid-svg-1dLJoBoidBfNSG9a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1dLJoBoidBfNSG9a .arrowheadPath{fill:#333333;}#mermaid-svg-1dLJoBoidBfNSG9a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1dLJoBoidBfNSG9a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1dLJoBoidBfNSG9a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1dLJoBoidBfNSG9a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1dLJoBoidBfNSG9a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1dLJoBoidBfNSG9a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1dLJoBoidBfNSG9a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1dLJoBoidBfNSG9a .cluster text{fill:#333;}#mermaid-svg-1dLJoBoidBfNSG9a .cluster span{color:#333;}#mermaid-svg-1dLJoBoidBfNSG9a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1dLJoBoidBfNSG9a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1dLJoBoidBfNSG9a rect.text{fill:none;stroke-width:0;}#mermaid-svg-1dLJoBoidBfNSG9a .icon-shape,#mermaid-svg-1dLJoBoidBfNSG9a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1dLJoBoidBfNSG9a .icon-shape p,#mermaid-svg-1dLJoBoidBfNSG9a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1dLJoBoidBfNSG9a .icon-shape .label rect,#mermaid-svg-1dLJoBoidBfNSG9a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1dLJoBoidBfNSG9a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1dLJoBoidBfNSG9a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1dLJoBoidBfNSG9a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
成功
失败
正常调用下游
统计失败率和慢调用
是否达到熔断阈值
打开熔断器
请求直接失败或执行降级
等待一段时间
放行少量试探请求
试探是否成功
需要区分两个概念:
- 熔断:决定当前请求是否还要调用下游服务。
- 降级:无法正常调用时,给用户返回什么结果。
熔断器发现下游服务持续异常时,会暂时切断调用;等待一段时间后,再放行少量请求检查服务是否恢复。
11.4 Gateway 接入熔断器
Gateway 本身不负责统计失败率和维护熔断状态,需要通过 Spring Cloud CircuitBreaker 接入 Resilience4j。
11.4.1 引入依赖
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-starter-circuitbreaker-reactor-resilience4j
</artifactId>
</dependency>
11.4.2 为 Route 配置熔断器
yaml
spring:
cloud:
gateway:
routes:
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order
参数含义:
name:熔断器名称,后续可以根据该名称配置失败率、等待时间等参数。fallbackUri:调用失败或熔断器打开时,转发到降级接口。
正常情况下:
text
请求 → Gateway → order-service
调用失败或熔断器已经打开时:
text
请求 → Gateway → /fallback/order
11.4.3 编写降级接口
java
@RestController
public class GatewayFallbackController {
@RequestMapping("/fallback/order")
public Mono<Map<String, Object>> orderFallback() {
return Mono.just(Map.of(
"code", 503,
"message", "订单服务暂时不可用,请稍后重试"
));
}
}
这里的降级接口并不是熔断器本身,它只是负责在无法正常调用 order-service 时,返回一个友好的结果。
需要区分:
- CircuitBreaker(熔断器):统计下游调用的成功、失败和慢调用情况,维护 Closed、Open、Half-Open 状态,并根据当前状态决定本次请求是否访问下游服务。
- fallback(降级处理):当下游调用失败、超时,或者熔断器已经打开时,向客户端返回预先定义的兜底结果。
WebFlux Gateway 的 fallbackUri 通常使用 forward:,表示在 Gateway 内部转发到本地接口:
yaml
fallbackUri: forward:/fallback/order
前面的熔断状态机解释"熔断器如何工作",这一部分解释"Gateway 如何配置和使用熔断器"。
11.5 限流和熔断的区别
#mermaid-svg-DNRhS090Gq4BTSg1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DNRhS090Gq4BTSg1 .error-icon{fill:#552222;}#mermaid-svg-DNRhS090Gq4BTSg1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DNRhS090Gq4BTSg1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DNRhS090Gq4BTSg1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DNRhS090Gq4BTSg1 .marker.cross{stroke:#333333;}#mermaid-svg-DNRhS090Gq4BTSg1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DNRhS090Gq4BTSg1 p{margin:0;}#mermaid-svg-DNRhS090Gq4BTSg1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster-label text{fill:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster-label span{color:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster-label span p{background-color:transparent;}#mermaid-svg-DNRhS090Gq4BTSg1 .label text,#mermaid-svg-DNRhS090Gq4BTSg1 span{fill:#333;color:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 .node rect,#mermaid-svg-DNRhS090Gq4BTSg1 .node circle,#mermaid-svg-DNRhS090Gq4BTSg1 .node ellipse,#mermaid-svg-DNRhS090Gq4BTSg1 .node polygon,#mermaid-svg-DNRhS090Gq4BTSg1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DNRhS090Gq4BTSg1 .rough-node .label text,#mermaid-svg-DNRhS090Gq4BTSg1 .node .label text,#mermaid-svg-DNRhS090Gq4BTSg1 .image-shape .label,#mermaid-svg-DNRhS090Gq4BTSg1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DNRhS090Gq4BTSg1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DNRhS090Gq4BTSg1 .rough-node .label,#mermaid-svg-DNRhS090Gq4BTSg1 .node .label,#mermaid-svg-DNRhS090Gq4BTSg1 .image-shape .label,#mermaid-svg-DNRhS090Gq4BTSg1 .icon-shape .label{text-align:center;}#mermaid-svg-DNRhS090Gq4BTSg1 .node.clickable{cursor:pointer;}#mermaid-svg-DNRhS090Gq4BTSg1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DNRhS090Gq4BTSg1 .arrowheadPath{fill:#333333;}#mermaid-svg-DNRhS090Gq4BTSg1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DNRhS090Gq4BTSg1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DNRhS090Gq4BTSg1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DNRhS090Gq4BTSg1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DNRhS090Gq4BTSg1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DNRhS090Gq4BTSg1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster text{fill:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 .cluster span{color:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DNRhS090Gq4BTSg1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DNRhS090Gq4BTSg1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DNRhS090Gq4BTSg1 .icon-shape,#mermaid-svg-DNRhS090Gq4BTSg1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DNRhS090Gq4BTSg1 .icon-shape p,#mermaid-svg-DNRhS090Gq4BTSg1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DNRhS090Gq4BTSg1 .icon-shape .label rect,#mermaid-svg-DNRhS090Gq4BTSg1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DNRhS090Gq4BTSg1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DNRhS090Gq4BTSg1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DNRhS090Gq4BTSg1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
请求进入
是否超过流量阈值
限流并返回429
熔断器是否打开
执行降级
调用下游服务
限流判断请求是不是太多;熔断判断下游服务是不是已经不健康。
12 Gateway 全局异常处理
12.1 为什么不能只依赖 @RestControllerAdvice
@RestControllerAdvice 可以处理进入 Controller 后抛出的异常,但 Gateway 中很多异常发生在以下阶段:
- Predicate 匹配。
- Filter Chain。
- 服务实例选择。
- 下游网络请求。
- 超时、限流和熔断。
这些异常可能没有进入任何 Controller,因此不能只依赖 @RestControllerAdvice,可以实现 ErrorWebExceptionHandler 或其他 WebExceptionHandler 覆盖整个 WebFlux 请求链路。
12.2 自定义全局异常处理
java
@Component
@Order(-2)
@RequiredArgsConstructor
public class GlobalErrorWebExceptionHandler
implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange,
Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
HttpStatusCode status = resolveStatus(ex);
response.setStatusCode(status);
response.getHeaders()
.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> result = Map.of(
"code", status.value(),
"message", resolveMessage(ex),
"path", exchange.getRequest().getPath().value()
);
try {
byte[] bytes =
objectMapper.writeValueAsBytes(result);
DataBuffer buffer =
response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException jsonException) {
return Mono.error(jsonException);
}
}
private HttpStatusCode resolveStatus(Throwable ex) {
if (ex instanceof ResponseStatusException exception) {
return exception.getStatusCode();
}
if (ex instanceof TimeoutException) {
return HttpStatus.GATEWAY_TIMEOUT;
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}
private String resolveMessage(Throwable ex) {
if (ex instanceof TimeoutException) {
return "下游服务响应超时";
}
return "网关请求处理失败";
}
}
这里也有
@Order(-2),但是!!它和前面的 Gateway Filter 不是一组,他们之间没有关系的,现在这个Order控制的是异常处理器链 的顺序,Spring Boot 默认 WebFlux 异常处理器通常为-1,因此设置为-2可以让自定义处理器先处理 Gateway 过滤器链中抛出的异常。
13 跨域 CORS
浏览器同源要求协议、域名和端口相同,例如前端 http://localhost:5173 访问后端 http://localhost:8080 属于跨域。
微服务 Gateway 浏览器 微服务 Gateway 浏览器 #mermaid-svg-xCmYyFtEk0JRTcVh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xCmYyFtEk0JRTcVh .error-icon{fill:#552222;}#mermaid-svg-xCmYyFtEk0JRTcVh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xCmYyFtEk0JRTcVh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xCmYyFtEk0JRTcVh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xCmYyFtEk0JRTcVh .marker.cross{stroke:#333333;}#mermaid-svg-xCmYyFtEk0JRTcVh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xCmYyFtEk0JRTcVh p{margin:0;}#mermaid-svg-xCmYyFtEk0JRTcVh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xCmYyFtEk0JRTcVh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xCmYyFtEk0JRTcVh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-xCmYyFtEk0JRTcVh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-xCmYyFtEk0JRTcVh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-xCmYyFtEk0JRTcVh .sequenceNumber{fill:white;}#mermaid-svg-xCmYyFtEk0JRTcVh #sequencenumber{fill:#333;}#mermaid-svg-xCmYyFtEk0JRTcVh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-xCmYyFtEk0JRTcVh .messageText{fill:#333;stroke:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xCmYyFtEk0JRTcVh .labelText,#mermaid-svg-xCmYyFtEk0JRTcVh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .loopText,#mermaid-svg-xCmYyFtEk0JRTcVh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xCmYyFtEk0JRTcVh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-xCmYyFtEk0JRTcVh .noteText,#mermaid-svg-xCmYyFtEk0JRTcVh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-xCmYyFtEk0JRTcVh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xCmYyFtEk0JRTcVh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xCmYyFtEk0JRTcVh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xCmYyFtEk0JRTcVh .actorPopupMenu{position:absolute;}#mermaid-svg-xCmYyFtEk0JRTcVh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-xCmYyFtEk0JRTcVh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xCmYyFtEk0JRTcVh .actor-man circle,#mermaid-svg-xCmYyFtEk0JRTcVh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-xCmYyFtEk0JRTcVh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OPTIONS预检请求返回允许的域名、方法和Header发送真实业务请求转发请求返回响应返回响应
4.x 配置示例:
yaml
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]':
allowedOrigins:
- "http://localhost:5173"
- "http://127.0.0.1:5173"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS # 打开
allowedHeaders:
- "*"
allowCredentials: true
maxAge: 3600
关于跨域的,这里不细聊了
13 业务灰度发布
按流量比例灰度
Weight Predicate 会按权重近似分配请求,但不会绑定用户,因此同一个用户这次可能进入 v1,下次可能进入 v2。官方示例中的 8:2 表示约 80% 和 20%。
yaml
spring:
cloud:
gateway:
routes:
- id: user-service-v1
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
- Weight=user-service-group, 90
- id: user-service-v2
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- Weight=user-service-group, 10
按用户稳定灰度
希望固定约 10% 的用户始终进入 v2,可以根据 userId 计算哈希桶:
text
bucket = hash(userId) % 100
bucket < 10 → v2
bucket >= 10 → v1
由于同一个 userId 的计算结果固定,因此同一用户会稳定进入同一版本。
断言不是根据条件负责转发请求到哪个微服务吗,但是官网没有提供按用户 ID 哈希取余的方式,所以我们需要自定义断言来自定义路由条件
先编写自定义 Predicate。Spring Cloud Gateway 支持通过继承 AbstractRoutePredicateFactory 实现自定义路由条件。
java
// 类名去掉 RoutePredicateFactory 就是名为 GrayUser 的路由判断条件
// 类名必须遵循:自定义名称 + RoutePredicateFactory
@Component
public class GrayUserRoutePredicateFactory
extends AbstractRoutePredicateFactory<
GrayUserRoutePredicateFactory.Config> {
private final TokenService tokenService;
public GrayUserRoutePredicateFactory(TokenService tokenService) {
super(Config.class);
this.tokenService = tokenService;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
/*
* 从已验签的 Token 中获取 userId。
* 这里只进行本地 Token 解析,不要查询数据库或执行阻塞调用。
*/
Long userId = tokenService.verifyAndGetUserId(
exchange.getRequest()
);
if (userId == null) {
return false;
}
int bucket = Math.floorMod(Long.hashCode(userId), 100);
return bucket < config.getPercentage();
};
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("percentage");
}
public static class Config {
private int percentage;
public int getPercentage() {
return percentage;
}
public void setPercentage(int percentage) {
this.percentage = percentage;
}
}
}
TokenService 表示项目已有的 Token 验签组件:
java
public interface TokenService {
/**
* 校验 Token 签名并返回可信 userId。
* 校验失败时返回 null。
*/
Long verifyAndGetUserId(ServerHttpRequest request);
}
然后配置两条 Route:
yaml
spring:
cloud:
gateway:
routes:
# 必须放在兜底 Route 前面
- id: user-service-v2
order: -10
uri: lb://user-service-v2
predicates:
- Path=/api/users/**
- GrayUser=10
# 没有命中灰度条件的请求进入 v1
- id: user-service-v1
order: 0
uri: lb://user-service-v1
predicates:
- Path=/api/users/**
GrayUser=10 表示哈希桶位于 [0, 9] 的用户进入 v2,其他用户由后面的 v1 Route 接收。
#mermaid-svg-i1xS7E5GrEITY1OB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-i1xS7E5GrEITY1OB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i1xS7E5GrEITY1OB .error-icon{fill:#552222;}#mermaid-svg-i1xS7E5GrEITY1OB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i1xS7E5GrEITY1OB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i1xS7E5GrEITY1OB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i1xS7E5GrEITY1OB .marker.cross{stroke:#333333;}#mermaid-svg-i1xS7E5GrEITY1OB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i1xS7E5GrEITY1OB p{margin:0;}#mermaid-svg-i1xS7E5GrEITY1OB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-i1xS7E5GrEITY1OB .cluster-label text{fill:#333;}#mermaid-svg-i1xS7E5GrEITY1OB .cluster-label span{color:#333;}#mermaid-svg-i1xS7E5GrEITY1OB .cluster-label span p{background-color:transparent;}#mermaid-svg-i1xS7E5GrEITY1OB .label text,#mermaid-svg-i1xS7E5GrEITY1OB span{fill:#333;color:#333;}#mermaid-svg-i1xS7E5GrEITY1OB .node rect,#mermaid-svg-i1xS7E5GrEITY1OB .node circle,#mermaid-svg-i1xS7E5GrEITY1OB .node ellipse,#mermaid-svg-i1xS7E5GrEITY1OB .node polygon,#mermaid-svg-i1xS7E5GrEITY1OB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i1xS7E5GrEITY1OB .rough-node .label text,#mermaid-svg-i1xS7E5GrEITY1OB .node .label text,#mermaid-svg-i1xS7E5GrEITY1OB .image-shape .label,#mermaid-svg-i1xS7E5GrEITY1OB .icon-shape .label{text-anchor:middle;}#mermaid-svg-i1xS7E5GrEITY1OB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i1xS7E5GrEITY1OB .rough-node .label,#mermaid-svg-i1xS7E5GrEITY1OB .node .label,#mermaid-svg-i1xS7E5GrEITY1OB .image-shape .label,#mermaid-svg-i1xS7E5GrEITY1OB .icon-shape .label{text-align:center;}#mermaid-svg-i1xS7E5GrEITY1OB .node.clickable{cursor:pointer;}#mermaid-svg-i1xS7E5GrEITY1OB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i1xS7E5GrEITY1OB .arrowheadPath{fill:#333333;}#mermaid-svg-i1xS7E5GrEITY1OB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i1xS7E5GrEITY1OB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i1xS7E5GrEITY1OB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i1xS7E5GrEITY1OB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i1xS7E5GrEITY1OB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i1xS7E5GrEITY1OB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i1xS7E5GrEITY1OB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i1xS7E5GrEITY1OB .cluster text{fill:#333;}#mermaid-svg-i1xS7E5GrEITY1OB .cluster span{color:#333;}#mermaid-svg-i1xS7E5GrEITY1OB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-i1xS7E5GrEITY1OB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i1xS7E5GrEITY1OB rect.text{fill:none;stroke-width:0;}#mermaid-svg-i1xS7E5GrEITY1OB .icon-shape,#mermaid-svg-i1xS7E5GrEITY1OB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i1xS7E5GrEITY1OB .icon-shape p,#mermaid-svg-i1xS7E5GrEITY1OB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i1xS7E5GrEITY1OB .icon-shape .label rect,#mermaid-svg-i1xS7E5GrEITY1OB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i1xS7E5GrEITY1OB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i1xS7E5GrEITY1OB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i1xS7E5GrEITY1OB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户请求
从已验签Token获取userId
计算 hash userId % 100
结果是否小于10
user-service-v2
user-service-v1
这里必须让 v2 Route 的 order 更小,因为 v1 只配置了 Path,会匹配所有 /api/users/** 请求;应先判断是否命中灰度,未命中时再进入 v1。
Weight适合随机按比例分流;用户 ID 哈希适合稳定灰度。同一个用户无论请求到哪个 Gateway 实例,只要哈希算法和灰度比例一致,都会得到相同的路由结果。
14 动态路由
静态路由通常直接写在 Gateway 的 application.yml 中:
yaml
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
Gateway 启动时读取这些配置。后续修改配置文件,通常需要重新发布配置或重启应用才能生效。动态路由则把 Route 定义存放在 Gateway 外部,例如:
- Nacos Config。
- Redis。
- 数据库。
- 路由管理平台。
修改外部配置后,各个 Gateway 实例重新加载 Route,不需要重启。可以搞一下 Nacos 的,比较方便
15 Gateway 高可用
如果只有一个 Gateway,该实例宕机后,即使所有微服务正常,外部请求也无法进入系统。
#mermaid-svg-vsFbZIZIcSeqkctL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vsFbZIZIcSeqkctL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vsFbZIZIcSeqkctL .error-icon{fill:#552222;}#mermaid-svg-vsFbZIZIcSeqkctL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vsFbZIZIcSeqkctL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vsFbZIZIcSeqkctL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vsFbZIZIcSeqkctL .marker.cross{stroke:#333333;}#mermaid-svg-vsFbZIZIcSeqkctL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vsFbZIZIcSeqkctL p{margin:0;}#mermaid-svg-vsFbZIZIcSeqkctL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vsFbZIZIcSeqkctL .cluster-label text{fill:#333;}#mermaid-svg-vsFbZIZIcSeqkctL .cluster-label span{color:#333;}#mermaid-svg-vsFbZIZIcSeqkctL .cluster-label span p{background-color:transparent;}#mermaid-svg-vsFbZIZIcSeqkctL .label text,#mermaid-svg-vsFbZIZIcSeqkctL span{fill:#333;color:#333;}#mermaid-svg-vsFbZIZIcSeqkctL .node rect,#mermaid-svg-vsFbZIZIcSeqkctL .node circle,#mermaid-svg-vsFbZIZIcSeqkctL .node ellipse,#mermaid-svg-vsFbZIZIcSeqkctL .node polygon,#mermaid-svg-vsFbZIZIcSeqkctL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vsFbZIZIcSeqkctL .rough-node .label text,#mermaid-svg-vsFbZIZIcSeqkctL .node .label text,#mermaid-svg-vsFbZIZIcSeqkctL .image-shape .label,#mermaid-svg-vsFbZIZIcSeqkctL .icon-shape .label{text-anchor:middle;}#mermaid-svg-vsFbZIZIcSeqkctL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vsFbZIZIcSeqkctL .rough-node .label,#mermaid-svg-vsFbZIZIcSeqkctL .node .label,#mermaid-svg-vsFbZIZIcSeqkctL .image-shape .label,#mermaid-svg-vsFbZIZIcSeqkctL .icon-shape .label{text-align:center;}#mermaid-svg-vsFbZIZIcSeqkctL .node.clickable{cursor:pointer;}#mermaid-svg-vsFbZIZIcSeqkctL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vsFbZIZIcSeqkctL .arrowheadPath{fill:#333333;}#mermaid-svg-vsFbZIZIcSeqkctL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vsFbZIZIcSeqkctL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vsFbZIZIcSeqkctL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vsFbZIZIcSeqkctL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vsFbZIZIcSeqkctL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vsFbZIZIcSeqkctL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vsFbZIZIcSeqkctL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vsFbZIZIcSeqkctL .cluster text{fill:#333;}#mermaid-svg-vsFbZIZIcSeqkctL .cluster span{color:#333;}#mermaid-svg-vsFbZIZIcSeqkctL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vsFbZIZIcSeqkctL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vsFbZIZIcSeqkctL rect.text{fill:none;stroke-width:0;}#mermaid-svg-vsFbZIZIcSeqkctL .icon-shape,#mermaid-svg-vsFbZIZIcSeqkctL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vsFbZIZIcSeqkctL .icon-shape p,#mermaid-svg-vsFbZIZIcSeqkctL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vsFbZIZIcSeqkctL .icon-shape .label rect,#mermaid-svg-vsFbZIZIcSeqkctL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vsFbZIZIcSeqkctL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vsFbZIZIcSeqkctL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vsFbZIZIcSeqkctL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端
Nginx或云负载均衡
Gateway-1
Gateway-2
Gateway-3
微服务集群
高可用需要做到:
- 多实例部署 Gateway。
- 通过 Nginx、云负载均衡或 Kubernetes Service 分发流量。
- 通过健康检查摘除故障实例。
- Gateway 尽量无状态。
- 登录状态存储在 Redis 等共享存储。
- 动态 Route 使用共享配置源。
- 分布式限流使用 Redis 等共享状态。(把令牌数、计数器放到 Redis)
- 配置连接超时、响应超时和熔断规则。
- 监控 CPU、内存、连接数、QPS、错误率和延迟。
如果每个 Gateway 实例使用本地内存限流,例如每个实例每秒允许 10 次,那么 3 个实例总共可能允许每秒 30 次;需要集群级严格限流时必须共享限流状态。
16 日志、监控和链路追踪
Gateway 是所有外部请求进入微服务系统的统一入口,因此适合记录请求的基本信息,例如:
- 请求方法和路径。
- 匹配到的 RouteId。
- 用户 ID 和客户端 IP。
- HTTP 状态码。
- 请求耗时。
- 转发到的下游服务。
- 异常信息。
日志示例:
text
traceId=9fd82a method=GET path=/api/orders/10001 routeId=order-service-route userId=10001 status=200 cost=53ms
这条日志表示:
traceId=9fd82a:本次请求的链路编号。method=GET:请求方法。path=/api/orders/10001:用户访问的地址。routeId=order-service-route:Gateway 匹配到的 Route。userId=10001:当前用户。status=200:请求成功。cost=53ms:整个请求耗时 53ms。
16.1 TraceId 与链路追踪
一次请求可能经过多个服务:
#mermaid-svg-a5uzsBGDMHfWWop1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-a5uzsBGDMHfWWop1 .error-icon{fill:#552222;}#mermaid-svg-a5uzsBGDMHfWWop1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-a5uzsBGDMHfWWop1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-a5uzsBGDMHfWWop1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-a5uzsBGDMHfWWop1 .marker.cross{stroke:#333333;}#mermaid-svg-a5uzsBGDMHfWWop1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-a5uzsBGDMHfWWop1 p{margin:0;}#mermaid-svg-a5uzsBGDMHfWWop1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster-label text{fill:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster-label span{color:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster-label span p{background-color:transparent;}#mermaid-svg-a5uzsBGDMHfWWop1 .label text,#mermaid-svg-a5uzsBGDMHfWWop1 span{fill:#333;color:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 .node rect,#mermaid-svg-a5uzsBGDMHfWWop1 .node circle,#mermaid-svg-a5uzsBGDMHfWWop1 .node ellipse,#mermaid-svg-a5uzsBGDMHfWWop1 .node polygon,#mermaid-svg-a5uzsBGDMHfWWop1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-a5uzsBGDMHfWWop1 .rough-node .label text,#mermaid-svg-a5uzsBGDMHfWWop1 .node .label text,#mermaid-svg-a5uzsBGDMHfWWop1 .image-shape .label,#mermaid-svg-a5uzsBGDMHfWWop1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-a5uzsBGDMHfWWop1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-a5uzsBGDMHfWWop1 .rough-node .label,#mermaid-svg-a5uzsBGDMHfWWop1 .node .label,#mermaid-svg-a5uzsBGDMHfWWop1 .image-shape .label,#mermaid-svg-a5uzsBGDMHfWWop1 .icon-shape .label{text-align:center;}#mermaid-svg-a5uzsBGDMHfWWop1 .node.clickable{cursor:pointer;}#mermaid-svg-a5uzsBGDMHfWWop1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-a5uzsBGDMHfWWop1 .arrowheadPath{fill:#333333;}#mermaid-svg-a5uzsBGDMHfWWop1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-a5uzsBGDMHfWWop1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-a5uzsBGDMHfWWop1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a5uzsBGDMHfWWop1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-a5uzsBGDMHfWWop1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a5uzsBGDMHfWWop1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster text{fill:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 .cluster span{color:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-a5uzsBGDMHfWWop1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-a5uzsBGDMHfWWop1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-a5uzsBGDMHfWWop1 .icon-shape,#mermaid-svg-a5uzsBGDMHfWWop1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a5uzsBGDMHfWWop1 .icon-shape p,#mermaid-svg-a5uzsBGDMHfWWop1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-a5uzsBGDMHfWWop1 .icon-shape .label rect,#mermaid-svg-a5uzsBGDMHfWWop1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a5uzsBGDMHfWWop1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-a5uzsBGDMHfWWop1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-a5uzsBGDMHfWWop1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Gateway
order-service
payment-service
RocketMQ消费者
如果每个服务都生成不同的日志编号,就无法判断哪些日志属于同一次请求。因此 Gateway 可以生成一个 TraceId,并将它通过请求头继续传递。
#mermaid-svg-NMs1mIrw52kVwmI7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NMs1mIrw52kVwmI7 .error-icon{fill:#552222;}#mermaid-svg-NMs1mIrw52kVwmI7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NMs1mIrw52kVwmI7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NMs1mIrw52kVwmI7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NMs1mIrw52kVwmI7 .marker.cross{stroke:#333333;}#mermaid-svg-NMs1mIrw52kVwmI7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NMs1mIrw52kVwmI7 p{margin:0;}#mermaid-svg-NMs1mIrw52kVwmI7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster-label text{fill:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster-label span{color:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster-label span p{background-color:transparent;}#mermaid-svg-NMs1mIrw52kVwmI7 .label text,#mermaid-svg-NMs1mIrw52kVwmI7 span{fill:#333;color:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 .node rect,#mermaid-svg-NMs1mIrw52kVwmI7 .node circle,#mermaid-svg-NMs1mIrw52kVwmI7 .node ellipse,#mermaid-svg-NMs1mIrw52kVwmI7 .node polygon,#mermaid-svg-NMs1mIrw52kVwmI7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NMs1mIrw52kVwmI7 .rough-node .label text,#mermaid-svg-NMs1mIrw52kVwmI7 .node .label text,#mermaid-svg-NMs1mIrw52kVwmI7 .image-shape .label,#mermaid-svg-NMs1mIrw52kVwmI7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-NMs1mIrw52kVwmI7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NMs1mIrw52kVwmI7 .rough-node .label,#mermaid-svg-NMs1mIrw52kVwmI7 .node .label,#mermaid-svg-NMs1mIrw52kVwmI7 .image-shape .label,#mermaid-svg-NMs1mIrw52kVwmI7 .icon-shape .label{text-align:center;}#mermaid-svg-NMs1mIrw52kVwmI7 .node.clickable{cursor:pointer;}#mermaid-svg-NMs1mIrw52kVwmI7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NMs1mIrw52kVwmI7 .arrowheadPath{fill:#333333;}#mermaid-svg-NMs1mIrw52kVwmI7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NMs1mIrw52kVwmI7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NMs1mIrw52kVwmI7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NMs1mIrw52kVwmI7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NMs1mIrw52kVwmI7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NMs1mIrw52kVwmI7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster text{fill:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 .cluster span{color:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NMs1mIrw52kVwmI7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NMs1mIrw52kVwmI7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-NMs1mIrw52kVwmI7 .icon-shape,#mermaid-svg-NMs1mIrw52kVwmI7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NMs1mIrw52kVwmI7 .icon-shape p,#mermaid-svg-NMs1mIrw52kVwmI7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NMs1mIrw52kVwmI7 .icon-shape .label rect,#mermaid-svg-NMs1mIrw52kVwmI7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NMs1mIrw52kVwmI7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NMs1mIrw52kVwmI7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NMs1mIrw52kVwmI7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} traceId=9fd82a
traceId=9fd82a
traceId=9fd82a
Gateway日志
order-service日志
payment-service日志
RocketMQ消费日志
这样出现问题时,可以使用同一个 TraceId 搜索整条调用链:
text
Gateway
→ order-service
→ payment-service
→ RocketMQ消费者
TraceId 用于把同一次请求经过的多个服务日志关联起来。
16.2 日志和监控指标的区别
日志记录每一次请求的详细信息:
text
traceId=9fd82a path=/api/orders/10001 userId=10001 cost=53ms
监控指标则用于统计整体情况,例如:
text
order-service-route 每秒有多少请求
5xx 错误率是多少
平均响应时间是多少
P99 响应时间是多少
因此:
| 内容 | 日志 | 监控指标 |
|---|---|---|
| TraceId | 适合 | 不适合 |
| 用户 ID | 可以记录 | 不适合作为标签 |
| 完整请求路径 | 可以记录 | 不适合作为标签 |
| RouteId | 可以记录 | 很适合作为标签 |
| HTTP 状态码 | 可以记录 | 很适合作为标签 |
| 请求耗时 | 记录单次耗时 | 统计平均值、P95、P99 |
16.3 什么是指标标签
监控系统通常会给指标附加标签,例如:
text
routeId=order-service-route
method=GET
status=200
这些标签可以用来分类统计:
text
order-service-route 的 GET 请求数量
order-service-route 的 5xx 错误数量
user-service-route 的平均响应时间
适合作为指标标签的内容应该只有有限的几种取值,例如:
- RouteId。
- HTTP Method。
- 状态码。
- 异常类型。
- 下游服务名。
16.4 什么是高基数问题
假设把完整请求路径作为指标标签:
text
/api/orders/10001
/api/orders/10002
/api/orders/10003
/api/orders/10004
每个订单 ID 都不同,监控系统会创建大量不同的时间序列:
text
path=/api/orders/10001
path=/api/orders/10002
path=/api/orders/10003
...
这种标签取值数量特别多的情况叫做高基数。
高基数会导致:
- 监控系统占用大量内存。
- 指标存储量快速增加。
- 查询速度变慢。
- Prometheus 等监控系统压力增大。
因此指标中不要使用完整路径、用户 ID、订单 ID、TraceId 等不断变化的值。
推荐使用固定的 RouteId:
text
routeId=order-service-route
或者归一化后的路径:
text
/api/orders/{id}
而不是:
text
/api/orders/10001
/api/orders/10002
需要注意,完整路径仍然可以写进日志,因为日志本来就是按请求逐条记录;只是不要把它作为监控指标标签。
日志用于查看某一次请求的详细过程;TraceId 用于关联整条调用链;监控指标用于观察系统整体状态。完整路径、用户 ID、TraceId 可以写进日志,但不适合作为指标标签。
17 项目中的完整 Gateway 设计
#mermaid-svg-CJtnn91EU75LZ2tu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CJtnn91EU75LZ2tu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CJtnn91EU75LZ2tu .error-icon{fill:#552222;}#mermaid-svg-CJtnn91EU75LZ2tu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CJtnn91EU75LZ2tu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CJtnn91EU75LZ2tu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CJtnn91EU75LZ2tu .marker.cross{stroke:#333333;}#mermaid-svg-CJtnn91EU75LZ2tu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CJtnn91EU75LZ2tu p{margin:0;}#mermaid-svg-CJtnn91EU75LZ2tu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CJtnn91EU75LZ2tu .cluster-label text{fill:#333;}#mermaid-svg-CJtnn91EU75LZ2tu .cluster-label span{color:#333;}#mermaid-svg-CJtnn91EU75LZ2tu .cluster-label span p{background-color:transparent;}#mermaid-svg-CJtnn91EU75LZ2tu .label text,#mermaid-svg-CJtnn91EU75LZ2tu span{fill:#333;color:#333;}#mermaid-svg-CJtnn91EU75LZ2tu .node rect,#mermaid-svg-CJtnn91EU75LZ2tu .node circle,#mermaid-svg-CJtnn91EU75LZ2tu .node ellipse,#mermaid-svg-CJtnn91EU75LZ2tu .node polygon,#mermaid-svg-CJtnn91EU75LZ2tu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CJtnn91EU75LZ2tu .rough-node .label text,#mermaid-svg-CJtnn91EU75LZ2tu .node .label text,#mermaid-svg-CJtnn91EU75LZ2tu .image-shape .label,#mermaid-svg-CJtnn91EU75LZ2tu .icon-shape .label{text-anchor:middle;}#mermaid-svg-CJtnn91EU75LZ2tu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CJtnn91EU75LZ2tu .rough-node .label,#mermaid-svg-CJtnn91EU75LZ2tu .node .label,#mermaid-svg-CJtnn91EU75LZ2tu .image-shape .label,#mermaid-svg-CJtnn91EU75LZ2tu .icon-shape .label{text-align:center;}#mermaid-svg-CJtnn91EU75LZ2tu .node.clickable{cursor:pointer;}#mermaid-svg-CJtnn91EU75LZ2tu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CJtnn91EU75LZ2tu .arrowheadPath{fill:#333333;}#mermaid-svg-CJtnn91EU75LZ2tu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CJtnn91EU75LZ2tu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CJtnn91EU75LZ2tu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CJtnn91EU75LZ2tu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CJtnn91EU75LZ2tu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CJtnn91EU75LZ2tu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CJtnn91EU75LZ2tu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CJtnn91EU75LZ2tu .cluster text{fill:#333;}#mermaid-svg-CJtnn91EU75LZ2tu .cluster span{color:#333;}#mermaid-svg-CJtnn91EU75LZ2tu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CJtnn91EU75LZ2tu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CJtnn91EU75LZ2tu rect.text{fill:none;stroke-width:0;}#mermaid-svg-CJtnn91EU75LZ2tu .icon-shape,#mermaid-svg-CJtnn91EU75LZ2tu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CJtnn91EU75LZ2tu .icon-shape p,#mermaid-svg-CJtnn91EU75LZ2tu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CJtnn91EU75LZ2tu .icon-shape .label rect,#mermaid-svg-CJtnn91EU75LZ2tu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CJtnn91EU75LZ2tu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CJtnn91EU75LZ2tu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CJtnn91EU75LZ2tu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
是
否
浏览器
Nginx
Gateway
生成TraceId
是否白名单
匹配Route
Sa-Token校验登录
Token是否合法
返回401
获取用户ID和通用权限
删除伪造Header并写入内部身份信息
是否超过限流阈值
返回429
Nacos获取服务实例
LoadBalancer选择实例
微服务
业务权限和数据权限校验
MySQL Redis RocketMQ
响应返回Gateway
记录状态码和耗时
Nginx返回客户端
完整步骤如下:
- Nginx 接收 HTTPS 请求并转发到 Gateway。
- Gateway 生成或透传 TraceId。
- Gateway 判断接口是否在白名单中。
- 非白名单接口使用 Sa-Token 校验登录。
- Gateway 从 Redis 获取 LoginId 和登录状态。
- Gateway 删除客户端提交的身份 Header。
- Gateway写入认证后的内部用户信息。
- Gateway 执行通用权限和接口限流。
- Gateway 按 Route 的 order 和 Predicate 匹配一条 Route。
- Gateway 合并 GlobalFilter 和 GatewayFilter。
- Gateway 根据
lb://service-name查询服务实例。 - Spring Cloud LoadBalancer 选择一个服务实例。
- Reactor Netty 将请求转发到下游。
- 微服务执行资源归属、数据权限和业务状态校验。
- 微服务访问 MySQL、Redis 或 RocketMQ。
- 响应经过 Post Filter 和 Nginx 返回客户端。
- Gateway 记录请求状态、耗时和异常信息。