Spring Cloud 商业场景训练营
这页把 Spring Cloud 放到真实商业系统里学习。不要只记住 Eureka、Feign、Gateway、Sentinel 这些名字,而要能画出一次请求怎么进入系统、怎么找到服务、怎么选择实例、怎么处理超时、怎么限流熔断、怎么排查线上问题。
本页场景统一用“医疗数据采集与资产平台 + 订单支付系统”来讲。因为这两类系统覆盖了微服务最常见的问题:网关入口、认证、服务发现、远程调用、配置中心、熔断降级、异步事件、链路追踪和故障排查。
训练目标
学完这页,你要能做到:
- 从客户端请求开始,完整解释 Gateway、注册中心、OpenFeign、LoadBalancer、Sentinel 和链路追踪如何协作。
- 解释服务发现为什么要注册、心跳、本地缓存、健康检查和元数据。
- 解释 OpenFeign 为什么只是代理,本质仍然是 HTTP 远程调用。
- 解释 Ribbon 和 Spring Cloud LoadBalancer 的关系,知道新旧项目怎么选。
- 解释限流、熔断、降级、超时、重试分别解决什么问题。
- 能写出最小可运行的服务调用、网关路由、负载均衡、超时和降级配置。
- 能按状态码、异常、traceId 和指标排查服务调用失败。
商业场景总览
以“资产详情页”为例,用户打开页面时需要聚合资产、医院、权限、字典和采集状态:
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 | 限流、熔断、降级 | 下游慢时拖垮上游,形成雪崩 |
| 配置中心 | 动态配置和环境隔离 | 修改阈值要重新发版,配置容易漂移 |
| Tracing | traceId 串联链路 | 线上只能翻多台机器日志靠猜 |
一次请求的完整执行流程
下面这张图不要画得过宽,学习时按编号看:
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 解码响应并返回"]核心理解:
lb://asset-service不是一个真实域名,它表示按服务名找实例。- Gateway 和 Feign 都可能触发负载均衡。
- 注册中心只提供服务实例列表,不转发业务请求。
- Feign 看起来像本地接口调用,但本质是网络调用,所以必须有超时、错误处理和降级。
- traceId 要从 Gateway 一直透传到所有下游,否则无法排查慢在哪。
Demo 一:最小服务注册与调用
订单服务配置
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库存服务配置
server:
port: 8082
spring:
application:
name: stock-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848Feign 接口
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);
}降级实现
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");
}
}调用方
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 要看懂四件事:
- 服务名是
stock-service,不是固定 IP。 - Feign 根据接口注解生成 HTTP 请求。
- LoadBalancer 从
stock-service的实例列表中选择一个。 - 下游不可用时不能让上游线程一直卡住,要走超时和降级。
服务发现原理
服务发现解决的是“服务地址会变化”的问题。容器重启、扩容、缩容、发布新版本都会导致实例地址变化。
flowchart TD
A["服务实例启动"] --> B["注册服务名、IP、端口、元数据"]
B --> C["定时心跳续约"]
C --> D["注册中心维护健康状态"]
E["调用方订阅服务"] --> F["拉取实例列表"]
F --> G["本地缓存实例"]
G --> H["负载均衡选择实例"]
D --> I["实例下线或心跳超时"]
I --> J["推送或等待调用方刷新"]为什么要本地缓存?
- 每次调用都实时访问注册中心,会把注册中心打成业务链路瓶颈。
- 注册中心短暂不可用时,调用方还能用缓存继续调用已有实例。
- 本地缓存能配合负载均衡快速选择实例。
不这样会怎样:
| 缺失能力 | 线上后果 |
|---|---|
| 无心跳 | 宕机实例迟迟不被剔除 |
| 无本地缓存 | 注册中心抖动导致全链路调用失败 |
| 无健康检查 | 服务进程活着但业务不可用,仍然被调用 |
| 无元数据 | 灰度、机房路由、版本隔离难做 |
OpenFeign 原理
Feign 的关键是“接口代理”。它不是把 Java 方法搬到远程 JVM 执行,而是根据接口、注解和参数拼出 HTTP 请求。
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 不设超时 | 下游慢会占满上游线程 | 设置连接超时和读取超时 |
| 写接口盲目重试 | 超时不代表失败,可能重复扣减 | 写接口先做幂等再考虑重试 |
| 循环调用 Feign | N 条数据调用 N 次下游 | 改成批量查询接口 |
| fallback 返回假成功 | 业务状态被污染 | 降级只能返回业务可接受的兜底 |
负载均衡:Ribbon 和 LoadBalancer
Spring Cloud 有两类常见负载均衡:
| 类型 | 代表技术 | 位置 | 说明 |
|---|---|---|---|
| 客户端负载均衡 | Ribbon、Spring Cloud LoadBalancer | 调用方进程内 | 调用方拿实例列表并选择 |
| 服务端负载均衡 | Nginx、SLB、Ingress、K8s Service | 统一代理层 | 调用方只访问代理入口 |
Ribbon 是 Netflix 老组件,老项目常见;Spring Cloud LoadBalancer 是当前 Spring Cloud 官方推荐。二者要解决的问题相同:从实例列表中选一台。
flowchart TD
A["调用方拿到实例列表"] --> B{"负载均衡策略"}
B --> C["轮询"]
B --> D["随机"]
B --> E["权重"]
B --> F["同机房优先"]
B --> G["元数据灰度"]
C --> H["选择目标实例"]
D --> H
E --> H
F --> H
G --> H为什么扩容后仍然负载不均?
- 调用方本地实例缓存还没刷新。
- 权重、元数据、灰度规则过滤后可选实例变少。
- 长连接或连接池复用导致流量集中。
- 某些请求按用户、租户、机构自然形成热点。
- 瓶颈不在应用实例,而在数据库、缓存或第三方接口。
Gateway 商业落地
Gateway 适合做入口横切能力,不适合写复杂业务。
配置 Demo
spring:
cloud:
gateway:
routes:
- id: asset-route
uri: lb://asset-service
predicates:
- Path=/api/assets/**
filters:
- StripPrefix=1
- name: RequestRateLimiter过滤器适合做什么
flowchart TD
A["请求进入 Gateway"] --> B["生成或读取 traceId"]
B --> C["认证登录态"]
C --> D["限流和黑白名单"]
D --> E["路由匹配"]
E --> F["转发下游服务"]
F --> G["记录响应状态和耗时"]Gateway 应该做:
- 登录态校验。
- 基础权限和租户校验。
- 限流、黑白名单。
- 跨域。
- traceId。
- 请求和响应日志。
- 灰度路由。
Gateway 不应该做:
- 下单事务。
- 扣库存。
- 支付状态流转。
- 复杂数据库查询。
- 领域规则判断。
原因很简单:网关是入口层,写太多业务会变成新的单体巨石,而且很难保证事务边界和领域职责清晰。
容错治理:超时、重试、限流、熔断、降级
这些概念经常被混在一起:
| 能力 | 触发时机 | 解决什么 | 错误用法 |
|---|---|---|---|
| 超时 | 等待太久 | 不无限占用线程 | 超时过长或完全不设 |
| 重试 | 偶发失败 | 抗短暂抖动 | 写操作无幂等就重试 |
| 限流 | 流量过大 | 保护自己 | 限流阈值拍脑袋 |
| 熔断 | 下游持续慢或错 | 快速失败,防雪崩 | 小故障就过度熔断 |
| 降级 | 非核心能力失败 | 返回可接受兜底 | 把失败伪装成成功 |
正确链路:
flowchart TD
A["请求进入"] --> B{"是否超过限流阈值"}
B -- "是" --> C["拒绝或排队"]
B -- "否" --> D{"熔断器是否打开"}
D -- "是" --> E["快速失败或降级"]
D -- "否" --> F["发起远程调用"]
F --> G{"是否超时或失败"}
G -- "否" --> H["返回成功"]
G -- "是" --> I["记录失败、可能重试"]
I --> J["失败率达到阈值后熔断"]业务边界:
- 查询字典失败,可以降级为空字典或缓存旧值。
- 推荐服务失败,可以返回默认推荐。
- 支付扣款失败,不能降级成“支付成功”。
- 库存扣减失败,不能降级成“有库存”。
配置中心怎么用才安全
配置中心适合管理多环境、多服务、可动态调整的参数。
| 配置类型 | 示例 | 注意 |
|---|---|---|
| 业务开关 | 某医院采集开关 | 要有默认值和审计 |
| 阈值 | 限流阈值、超时时间 | 先灰度再全量 |
| 批处理参数 | 每批采集数量 | 配错会影响吞吐 |
| 路由灰度 | version、tag、weight | 要能快速回滚 |
| 第三方地址 | 医院接口地址 | 要避免明文密钥 |
动态配置不是万能的。错误配置如果立即推到全量服务,会比发版事故更快扩散。所以生产上要有:
- 命名空间隔离环境。
- Group 或 DataId 区分服务。
- 配置校验。
- 变更审计。
- 灰度发布。
- 回滚方案。
链路追踪排查
没有 traceId,微服务排查会变成“每台机器都翻一遍日志”。
flowchart TD
A["Gateway 生成 traceId"] --> B["写入请求头"]
B --> C["asset-service 日志打印 traceId"]
C --> D["Feign 拦截器继续透传"]
D --> E["dict-service 日志打印同一 traceId"]
D --> F["collect-service 日志打印同一 traceId"]排查一次慢请求:
- 先从 Gateway access log 找 traceId、路径、状态码、总耗时。
- 到链路追踪平台看慢 span。
- 如果慢在 Feign 等待,看下游服务。
- 如果慢在下游业务方法,看 SQL、Redis、线程池、GC。
- 如果状态码是 503,看注册中心实例和网关路由。
- 如果状态码是 504,看网关、Feign、下游超时配置。
故障排查决策树
Gateway 404
flowchart TD
A["Gateway 404"] --> B["Path 是否匹配 Route"]
B --> C["Predicate 是否满足"]
C --> D["StripPrefix 是否配置错误"]
D --> E["后端 Controller 路径是否一致"]Gateway 503 或 Feign 无实例
flowchart TD
A["无可用实例"] --> B["服务名是否拼错"]
B --> C["namespace/group 是否一致"]
C --> D["注册中心是否有实例"]
D --> E["实例健康状态是否 UP"]
E --> F["灰度或元数据过滤后是否为空"]
F --> G["调用方本地缓存是否刷新"]调用超时
flowchart TD
A["调用超时"] --> B["connect timeout 还是 read timeout"]
B --> C["网络和端口是否通"]
C --> D["下游线程池是否满"]
D --> E["下游 DB/Redis/外部接口是否慢"]
E --> F["是否有重试放大流量"]
F --> G["是否触发熔断和降级"]商业场景一:资产详情聚合接口
需求:资产详情页要展示资产主体、医院名称、责任人、采集状态和字典翻译。
推荐设计:
asset-service负责资产事实数据。user-service批量查询责任人,避免循环 Feign。dict-service提供字典批量查询和本地缓存。collect-service只返回采集状态摘要。- 非核心字段可以降级,资产主体不能降级成假数据。
错误示例:
for (Asset asset : assets) {
UserDTO user = userClient.getUser(asset.getOwnerId());
DictDTO dict = dictClient.getDict(asset.getType());
}问题:列表 100 条会触发 200 次远程调用,延迟和失败率都会被放大。
改成批量接口:
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);商业场景二:采集任务调用医院接口
医疗采集服务调用外部医院接口时,外部接口可能慢、失败、限流或返回异常。
推荐链路:
flowchart TD
A["XXL-JOB 触发采集任务"] --> B["collect-service"]
B --> C["读取配置中心采集开关和批大小"]
C --> D["调用 hospital-adapter-service"]
D --> E{"医院接口是否成功"}
E -- "成功" --> F["原始数据落库"]
E -- "失败" --> G["记录失败批次"]
G --> H["重试、补偿或人工处理"]关键配置:
- 医院接口必须设置连接和读取超时。
- 失败批次要落库,不能只打印日志。
- 采集任务要有
taskId + hospitalId + batchNo幂等键。 - 慢医院要单独限流或熔断,不能拖垮所有采集。
- 参数放配置中心,但变更要审计。
面试标准回答
Spring Cloud 服务调用链路怎么说
请求通常先进入 Spring Cloud Gateway,网关执行认证、限流、日志和路由,根据 lb:// 服务名找到目标服务。服务启动时会把服务名、IP、端口和元数据注册到注册中心,调用方本地缓存实例列表。服务间调用常用 OpenFeign,Feign 通过动态代理把接口方法转换为 HTTP 请求,再交给 Spring Cloud LoadBalancer 从实例列表中选一个实例。生产环境还要配置超时、重试、熔断、降级和链路追踪,避免下游故障拖垮上游,并能通过 traceId 定位问题。Spring Cloud 负载均衡有哪些
Spring Cloud 里常见客户端负载均衡,老项目常见 Ribbon,新项目常见 Spring Cloud LoadBalancer。客户端负载均衡由调用方从注册中心获取实例列表,然后本地选择一个实例发起请求。入口层也会使用 Nginx、SLB、Ingress 或 Kubernetes Service 做服务端负载均衡。二者边界不同:客户端负载均衡更贴近服务发现和元数据路由,服务端负载均衡更适合统一入口和基础转发。下游慢了怎么治理
先设置合理的连接超时和读取超时,避免线程无限等待。查询类接口可以在幂等前提下少量重试,写接口必须先保证幂等再考虑重试。然后用 Sentinel 或 Resilience4j 做限流、熔断和降级:限流保护自己,熔断在下游持续慢或错误时快速失败,降级返回业务可接受的兜底结果。排查时通过 traceId 找慢 span,再看下游线程池、数据库、缓存、GC 和第三方接口。