Skip to content

Spring Cloud 商业场景训练营

这页把 Spring Cloud 放到真实商业系统里学习。不要只记住 Eureka、Feign、Gateway、Sentinel 这些名字,而要能画出一次请求怎么进入系统、怎么找到服务、怎么选择实例、怎么处理超时、怎么限流熔断、怎么排查线上问题。

本页场景统一用“医疗数据采集与资产平台 + 订单支付系统”来讲。因为这两类系统覆盖了微服务最常见的问题:网关入口、认证、服务发现、远程调用、配置中心、熔断降级、异步事件、链路追踪和故障排查。

训练目标

学完这页,你要能做到:

  1. 从客户端请求开始,完整解释 Gateway、注册中心、OpenFeign、LoadBalancer、Sentinel 和链路追踪如何协作。
  2. 解释服务发现为什么要注册、心跳、本地缓存、健康检查和元数据。
  3. 解释 OpenFeign 为什么只是代理,本质仍然是 HTTP 远程调用。
  4. 解释 Ribbon 和 Spring Cloud LoadBalancer 的关系,知道新旧项目怎么选。
  5. 解释限流、熔断、降级、超时、重试分别解决什么问题。
  6. 能写出最小可运行的服务调用、网关路由、负载均衡、超时和降级配置。
  7. 能按状态码、异常、traceId 和指标排查服务调用失败。

商业场景总览

以“资产详情页”为例,用户打开页面时需要聚合资产、医院、权限、字典和采集状态:

mermaid
flowchart TD
    A["前端请求资产详情"] --> B["Spring Cloud Gateway"]
    B --> C["认证、限流、traceId"]
    C --> D["路由到 asset-service"]
    D --> E["资产服务查询资产库"]
    D --> F["OpenFeign 调 user-service"]
    D --> G["OpenFeign 调 dict-service"]
    D --> H["OpenFeign 调 collect-service"]
    F --> I["LoadBalancer 选择用户实例"]
    G --> J["LoadBalancer 选择字典实例"]
    H --> K["LoadBalancer 选择采集实例"]

这条链路里每一步都有生产问题:

环节解决什么没做好会怎样
Gateway统一入口、鉴权、限流、路由每个服务暴露入口,权限和流量治理分散
注册中心服务名找到实例IP 写死,扩容和故障迁移困难
LoadBalancer多实例选择一个某台机器被打爆,或者调到已下线实例
OpenFeign声明式远程 HTTP 调用手写 HTTP 代码重复,错误处理不统一
Sentinel/Resilience4j限流、熔断、降级下游慢时拖垮上游,形成雪崩
配置中心动态配置和环境隔离修改阈值要重新发版,配置容易漂移
TracingtraceId 串联链路线上只能翻多台机器日志靠猜

一次请求的完整执行流程

下面这张图不要画得过宽,学习时按编号看:

mermaid
flowchart TD
    A["1 客户端发请求"] --> B["2 Gateway 匹配 Route"]
    B --> C["3 执行认证、限流、日志过滤器"]
    C --> D["4 lb://asset-service"]
    D --> E["5 查询或使用本地缓存实例列表"]
    E --> F["6 LoadBalancer 选择实例"]
    F --> G["7 转发到资产服务"]
    G --> H["8 资产服务调用 Feign 接口"]
    H --> I["9 Feign 生成 HTTP 请求"]
    I --> J["10 再次负载均衡选择下游实例"]
    J --> K["11 HTTP Client 发送请求"]
    K --> L["12 解码响应并返回"]

核心理解:

  1. lb://asset-service 不是一个真实域名,它表示按服务名找实例。
  2. Gateway 和 Feign 都可能触发负载均衡。
  3. 注册中心只提供服务实例列表,不转发业务请求。
  4. Feign 看起来像本地接口调用,但本质是网络调用,所以必须有超时、错误处理和降级。
  5. traceId 要从 Gateway 一直透传到所有下游,否则无法排查慢在哪。

Demo 一:最小服务注册与调用

订单服务配置

yaml
server:
  port: 8081

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    openfeign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000

库存服务配置

yaml
server:
  port: 8082

spring:
  application:
    name: stock-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

Feign 接口

java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "stock-service", fallback = StockClientFallback.class)
public interface StockClient {

    @GetMapping("/stocks/{skuId}")
    StockDTO getStock(@PathVariable("skuId") Long skuId);
}

降级实现

java
import org.springframework.stereotype.Component;

@Component
public class StockClientFallback implements StockClient {

    @Override
    public StockDTO getStock(Long skuId) {
        return new StockDTO(skuId, 0, "STOCK_SERVICE_UNAVAILABLE");
    }
}

调用方

java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    private final StockClient stockClient;

    public OrderController(StockClient stockClient) {
        this.stockClient = stockClient;
    }

    @GetMapping("/orders/preview/{skuId}")
    public OrderPreviewVO preview(@PathVariable Long skuId) {
        StockDTO stock = stockClient.getStock(skuId);
        boolean canBuy = stock.available() > 0;
        return new OrderPreviewVO(skuId, canBuy, stock.status());
    }
}

这个 Demo 要看懂四件事:

  1. 服务名是 stock-service,不是固定 IP。
  2. Feign 根据接口注解生成 HTTP 请求。
  3. LoadBalancer 从 stock-service 的实例列表中选择一个。
  4. 下游不可用时不能让上游线程一直卡住,要走超时和降级。

服务发现原理

服务发现解决的是“服务地址会变化”的问题。容器重启、扩容、缩容、发布新版本都会导致实例地址变化。

mermaid
flowchart TD
    A["服务实例启动"] --> B["注册服务名、IP、端口、元数据"]
    B --> C["定时心跳续约"]
    C --> D["注册中心维护健康状态"]
    E["调用方订阅服务"] --> F["拉取实例列表"]
    F --> G["本地缓存实例"]
    G --> H["负载均衡选择实例"]
    D --> I["实例下线或心跳超时"]
    I --> J["推送或等待调用方刷新"]

为什么要本地缓存?

  1. 每次调用都实时访问注册中心,会把注册中心打成业务链路瓶颈。
  2. 注册中心短暂不可用时,调用方还能用缓存继续调用已有实例。
  3. 本地缓存能配合负载均衡快速选择实例。

不这样会怎样:

缺失能力线上后果
无心跳宕机实例迟迟不被剔除
无本地缓存注册中心抖动导致全链路调用失败
无健康检查服务进程活着但业务不可用,仍然被调用
无元数据灰度、机房路由、版本隔离难做

OpenFeign 原理

Feign 的关键是“接口代理”。它不是把 Java 方法搬到远程 JVM 执行,而是根据接口、注解和参数拼出 HTTP 请求。

mermaid
flowchart TD
    A["业务代码调用 Feign 接口"] --> B["代理对象拦截方法调用"]
    B --> C["读取 @GetMapping、@PostMapping"]
    C --> D["解析 PathVariable、RequestBody"]
    D --> E["生成 RequestTemplate"]
    E --> F["按服务名选择实例"]
    F --> G["HTTP Client 发送请求"]
    G --> H["Decoder 反序列化响应"]
    H --> I["返回 Java 对象"]

常见错误:

错误原因正确做法
Feign 不设超时下游慢会占满上游线程设置连接超时和读取超时
写接口盲目重试超时不代表失败,可能重复扣减写接口先做幂等再考虑重试
循环调用 FeignN 条数据调用 N 次下游改成批量查询接口
fallback 返回假成功业务状态被污染降级只能返回业务可接受的兜底

负载均衡:Ribbon 和 LoadBalancer

Spring Cloud 有两类常见负载均衡:

类型代表技术位置说明
客户端负载均衡Ribbon、Spring Cloud LoadBalancer调用方进程内调用方拿实例列表并选择
服务端负载均衡Nginx、SLB、Ingress、K8s Service统一代理层调用方只访问代理入口

Ribbon 是 Netflix 老组件,老项目常见;Spring Cloud LoadBalancer 是当前 Spring Cloud 官方推荐。二者要解决的问题相同:从实例列表中选一台。

mermaid
flowchart TD
    A["调用方拿到实例列表"] --> B{"负载均衡策略"}
    B --> C["轮询"]
    B --> D["随机"]
    B --> E["权重"]
    B --> F["同机房优先"]
    B --> G["元数据灰度"]
    C --> H["选择目标实例"]
    D --> H
    E --> H
    F --> H
    G --> H

为什么扩容后仍然负载不均?

  1. 调用方本地实例缓存还没刷新。
  2. 权重、元数据、灰度规则过滤后可选实例变少。
  3. 长连接或连接池复用导致流量集中。
  4. 某些请求按用户、租户、机构自然形成热点。
  5. 瓶颈不在应用实例,而在数据库、缓存或第三方接口。

Gateway 商业落地

Gateway 适合做入口横切能力,不适合写复杂业务。

配置 Demo

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: asset-route
          uri: lb://asset-service
          predicates:
            - Path=/api/assets/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter

过滤器适合做什么

mermaid
flowchart TD
    A["请求进入 Gateway"] --> B["生成或读取 traceId"]
    B --> C["认证登录态"]
    C --> D["限流和黑白名单"]
    D --> E["路由匹配"]
    E --> F["转发下游服务"]
    F --> G["记录响应状态和耗时"]

Gateway 应该做:

  1. 登录态校验。
  2. 基础权限和租户校验。
  3. 限流、黑白名单。
  4. 跨域。
  5. traceId。
  6. 请求和响应日志。
  7. 灰度路由。

Gateway 不应该做:

  1. 下单事务。
  2. 扣库存。
  3. 支付状态流转。
  4. 复杂数据库查询。
  5. 领域规则判断。

原因很简单:网关是入口层,写太多业务会变成新的单体巨石,而且很难保证事务边界和领域职责清晰。

容错治理:超时、重试、限流、熔断、降级

这些概念经常被混在一起:

能力触发时机解决什么错误用法
超时等待太久不无限占用线程超时过长或完全不设
重试偶发失败抗短暂抖动写操作无幂等就重试
限流流量过大保护自己限流阈值拍脑袋
熔断下游持续慢或错快速失败,防雪崩小故障就过度熔断
降级非核心能力失败返回可接受兜底把失败伪装成成功

正确链路:

mermaid
flowchart TD
    A["请求进入"] --> B{"是否超过限流阈值"}
    B -- "是" --> C["拒绝或排队"]
    B -- "否" --> D{"熔断器是否打开"}
    D -- "是" --> E["快速失败或降级"]
    D -- "否" --> F["发起远程调用"]
    F --> G{"是否超时或失败"}
    G -- "否" --> H["返回成功"]
    G -- "是" --> I["记录失败、可能重试"]
    I --> J["失败率达到阈值后熔断"]

业务边界:

  1. 查询字典失败,可以降级为空字典或缓存旧值。
  2. 推荐服务失败,可以返回默认推荐。
  3. 支付扣款失败,不能降级成“支付成功”。
  4. 库存扣减失败,不能降级成“有库存”。

配置中心怎么用才安全

配置中心适合管理多环境、多服务、可动态调整的参数。

配置类型示例注意
业务开关某医院采集开关要有默认值和审计
阈值限流阈值、超时时间先灰度再全量
批处理参数每批采集数量配错会影响吞吐
路由灰度version、tag、weight要能快速回滚
第三方地址医院接口地址要避免明文密钥

动态配置不是万能的。错误配置如果立即推到全量服务,会比发版事故更快扩散。所以生产上要有:

  1. 命名空间隔离环境。
  2. Group 或 DataId 区分服务。
  3. 配置校验。
  4. 变更审计。
  5. 灰度发布。
  6. 回滚方案。

链路追踪排查

没有 traceId,微服务排查会变成“每台机器都翻一遍日志”。

mermaid
flowchart TD
    A["Gateway 生成 traceId"] --> B["写入请求头"]
    B --> C["asset-service 日志打印 traceId"]
    C --> D["Feign 拦截器继续透传"]
    D --> E["dict-service 日志打印同一 traceId"]
    D --> F["collect-service 日志打印同一 traceId"]

排查一次慢请求:

  1. 先从 Gateway access log 找 traceId、路径、状态码、总耗时。
  2. 到链路追踪平台看慢 span。
  3. 如果慢在 Feign 等待,看下游服务。
  4. 如果慢在下游业务方法,看 SQL、Redis、线程池、GC。
  5. 如果状态码是 503,看注册中心实例和网关路由。
  6. 如果状态码是 504,看网关、Feign、下游超时配置。

故障排查决策树

Gateway 404

mermaid
flowchart TD
    A["Gateway 404"] --> B["Path 是否匹配 Route"]
    B --> C["Predicate 是否满足"]
    C --> D["StripPrefix 是否配置错误"]
    D --> E["后端 Controller 路径是否一致"]

Gateway 503 或 Feign 无实例

mermaid
flowchart TD
    A["无可用实例"] --> B["服务名是否拼错"]
    B --> C["namespace/group 是否一致"]
    C --> D["注册中心是否有实例"]
    D --> E["实例健康状态是否 UP"]
    E --> F["灰度或元数据过滤后是否为空"]
    F --> G["调用方本地缓存是否刷新"]

调用超时

mermaid
flowchart TD
    A["调用超时"] --> B["connect timeout 还是 read timeout"]
    B --> C["网络和端口是否通"]
    C --> D["下游线程池是否满"]
    D --> E["下游 DB/Redis/外部接口是否慢"]
    E --> F["是否有重试放大流量"]
    F --> G["是否触发熔断和降级"]

商业场景一:资产详情聚合接口

需求:资产详情页要展示资产主体、医院名称、责任人、采集状态和字典翻译。

推荐设计:

  1. asset-service 负责资产事实数据。
  2. user-service 批量查询责任人,避免循环 Feign。
  3. dict-service 提供字典批量查询和本地缓存。
  4. collect-service 只返回采集状态摘要。
  5. 非核心字段可以降级,资产主体不能降级成假数据。

错误示例:

java
for (Asset asset : assets) {
    UserDTO user = userClient.getUser(asset.getOwnerId());
    DictDTO dict = dictClient.getDict(asset.getType());
}

问题:列表 100 条会触发 200 次远程调用,延迟和失败率都会被放大。

改成批量接口:

java
List<Long> ownerIds = assets.stream().map(Asset::getOwnerId).distinct().toList();
Map<Long, UserDTO> users = userClient.listByIds(ownerIds);

List<String> dictKeys = assets.stream().map(Asset::getType).distinct().toList();
Map<String, DictDTO> dicts = dictClient.listByKeys(dictKeys);

商业场景二:采集任务调用医院接口

医疗采集服务调用外部医院接口时,外部接口可能慢、失败、限流或返回异常。

推荐链路:

mermaid
flowchart TD
    A["XXL-JOB 触发采集任务"] --> B["collect-service"]
    B --> C["读取配置中心采集开关和批大小"]
    C --> D["调用 hospital-adapter-service"]
    D --> E{"医院接口是否成功"}
    E -- "成功" --> F["原始数据落库"]
    E -- "失败" --> G["记录失败批次"]
    G --> H["重试、补偿或人工处理"]

关键配置:

  1. 医院接口必须设置连接和读取超时。
  2. 失败批次要落库,不能只打印日志。
  3. 采集任务要有 taskId + hospitalId + batchNo 幂等键。
  4. 慢医院要单独限流或熔断,不能拖垮所有采集。
  5. 参数放配置中心,但变更要审计。

面试标准回答

Spring Cloud 服务调用链路怎么说

text
请求通常先进入 Spring Cloud Gateway,网关执行认证、限流、日志和路由,根据 lb:// 服务名找到目标服务。服务启动时会把服务名、IP、端口和元数据注册到注册中心,调用方本地缓存实例列表。服务间调用常用 OpenFeign,Feign 通过动态代理把接口方法转换为 HTTP 请求,再交给 Spring Cloud LoadBalancer 从实例列表中选一个实例。生产环境还要配置超时、重试、熔断、降级和链路追踪,避免下游故障拖垮上游,并能通过 traceId 定位问题。

Spring Cloud 负载均衡有哪些

text
Spring Cloud 里常见客户端负载均衡,老项目常见 Ribbon,新项目常见 Spring Cloud LoadBalancer。客户端负载均衡由调用方从注册中心获取实例列表,然后本地选择一个实例发起请求。入口层也会使用 Nginx、SLB、Ingress 或 Kubernetes Service 做服务端负载均衡。二者边界不同:客户端负载均衡更贴近服务发现和元数据路由,服务端负载均衡更适合统一入口和基础转发。

下游慢了怎么治理

text
先设置合理的连接超时和读取超时,避免线程无限等待。查询类接口可以在幂等前提下少量重试,写接口必须先保证幂等再考虑重试。然后用 Sentinel 或 Resilience4j 做限流、熔断和降级:限流保护自己,熔断在下游持续慢或错误时快速失败,降级返回业务可接受的兜底结果。排查时通过 traceId 找慢 span,再看下游线程池、数据库、缓存、GC 和第三方接口。

关联知识点