Spring Cloud Alibaba 从零到生产级组件全景
一、这门课解决什么问题
Spring Cloud Alibaba 不是一个“万能微服务框架”,也不是把所有 Alibaba 中间件装进项目。它是一组与 Spring Boot、Spring Cloud 编程模型集成的组件和适配层,帮助应用完成服务发现、配置管理、流量治理、消息驱动和分布式事务等工作。
零基础先记住下面这句话:
Spring Cloud 规定很多微服务能力的抽象和编程方式;Spring Cloud Alibaba 为其中一部分能力提供 Alibaba 生态实现。业务请求仍由 Gateway、HTTP/RPC 客户端和业务服务处理,不会每次都经过 Nacos。
阅读入口:先看 Cloud 抽象,再看 Alibaba 实现
按“版本 BOM → Starter 自动配置 → Nacos/Sentinel/RocketMQ/Seata 分组件 → 全链路协作”阅读。Alibaba 不是一个单体组件,遇到问题要回到具体实现的控制面和数据面。
核心原理:Cloud 抽象与 Alibaba 实现的适配层
Alibaba Starter 通过自动配置把 Nacos、Sentinel、RocketMQ、Seata 等实现接入 Spring Cloud 的 Discovery、Config、CircuitBreaker、Stream 和事务抽象。应用依赖的是 Cloud 接口,运行时由 Starter 注册相应客户端、拦截器和规则数据源。
flowchart TD
A["Spring Boot应用"] --> B["Spring Cloud抽象"]
B --> C["Alibaba Starter自动配置"]
C --> D["Nacos Discovery/Config"]
C --> E["Sentinel流量规则"]
C --> F["RocketMQ Stream Binder"]
C --> G["Seata事务协调"]
D --> H["控制面配置与实例"]
E --> I["调用放行/拒绝"]
F --> J["事件投递"]
G --> K["分布式事务状态"]学完本页,你应该能够回答:
- Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者是什么关系。
- 为什么必须同时管理 Spring Cloud BOM 和 Spring Cloud Alibaba BOM。
- Nacos、Sentinel、OpenFeign、LoadBalancer、Gateway、RocketMQ、Seata、Dubbo 各负责什么。
- 一个微服务从读取远程配置到注册成功,内部经历哪些阶段。
- 一个外部请求从 Gateway 到服务提供者,完整调用链是什么。
- 注册中心为什么不在每次业务请求的数据通路上。
- Sentinel 为什么既能限流又能熔断,
blockHandler和fallback有什么不同。 - 配置更新为什么不等于所有对象立即、原子地使用新配置。
- RocketMQ 事务消息、Seata 和消费幂等分别解决什么,为什么不能互相替代。
- 生产环境如何设计高可用、隔离、可观测性和故障排查顺序。
二、先建立正确的生态位置
flowchart TD
A["Spring Boot<br/>应用启动、自动配置、依赖管理基础"] --> B["Spring Cloud<br/>微服务抽象与通用组件"]
B --> C["Spring Cloud Alibaba<br/>Alibaba 生态适配与集成"]
C --> D["Nacos<br/>注册发现与配置"]
C --> E["Sentinel<br/>流量与熔断治理"]
C --> F["RocketMQ Binder<br/>消息驱动集成"]
C --> G["Seata 集成<br/>分布式事务"]
C --> H["Dubbo 集成<br/>RPC 能力"]三者不能随意混配:
| 层次 | 主要职责 | 典型内容 |
|---|---|---|
| Spring Boot | 建立单个应用 | 自动配置、Starter、配置绑定、内嵌容器、Actuator |
| Spring Cloud | 建立微服务通用抽象 | DiscoveryClient、LoadBalancer、OpenFeign、Gateway、Config Data、Stream |
| Spring Cloud Alibaba | 对接 Alibaba 生态 | Nacos Discovery/Config、Sentinel、RocketMQ Binder、Seata/Dubbo 集成 |
| 中间件服务端 | 真正保存和处理数据 | Nacos Server、RocketMQ Broker、Seata TC、Sentinel Dashboard |
必须区分“客户端依赖”和“服务端集群”。例如引入 Nacos Discovery Starter 只是在应用里加入 Nacos 客户端;它不会自动创建 Nacos Server。
三、组件职责地图:先按问题学,不按名字背
| 问题 | 主线组件 | 真正处理的事情 | 不负责什么 |
|---|---|---|---|
| 服务地址会变化 | Nacos Discovery | 注册实例、订阅服务、维护实例快照 | 不代理每次 HTTP 请求 |
| 配置分散 | Nacos Config | 定位、发布、拉取、监听远程配置 | 不保证任意 Bean 都可无损热更新 |
| Java 服务间调用 | OpenFeign | 根据接口创建代理并组装 HTTP 请求 | 不保存服务实例,不天然保证幂等 |
| 多实例选择 | Spring Cloud LoadBalancer | 从实例快照中选择目标实例 | 不自动判断业务结果是否成功 |
| 外部统一入口 | Spring Cloud Gateway | 路由匹配、过滤、鉴权、限流、转发 | 不应该承载核心业务事务 |
| 流量过大或下游故障 | Sentinel | 统计资源、匹配规则、限流与熔断 | 降级结果不等于业务成功 |
| 异步解耦 | RocketMQ / Stream Binder | 消息存储、投递、重试和消费集成 | 不能自动做到跨数据库“恰好一次” |
| 跨服务数据一致性 | Seata | 协调 AT、XA、TCC、Saga 分支 | 不能替代业务幂等和补偿设计 |
| 高性能 RPC | Dubbo | 接口代理、RPC 协议、编解码、路由治理 | 不是所有 HTTP 服务都必须改成 RPC |
| 定位跨服务问题 | Micrometer、OpenTelemetry、SkyWalking | 传播上下文、采集 Trace/Metrics/Logs | 不能修复业务错误,只提供证据 |
推荐理解方式是:注册发现提供地址,负载均衡选择地址,调用客户端发出请求,Sentinel保护资源,追踪系统留下证据。
四、版本与 BOM:代码没错也可能启动不了
4.1 为什么有两个 BOM
BOM(Bill of Materials)是一组经过协调的依赖版本。Spring Cloud 和 Spring Cloud Alibaba 是两个发布体系,因此通常要同时导入:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Maven 导入 BOM 后,并没有把所有组件都下载下来。它只是把“某个依赖若被使用,默认该用哪个版本”放入 dependencyManagement。真正写进 <dependencies> 的 Starter 才会进入依赖树。若业务 POM 又显式指定版本,或者另一个依赖传递进来不同版本,最终结果由 Maven 的依赖调解规则决定,所以还要看 dependency:tree,不能只看 BOM 文件。
flowchart TD
A["父 POM 与两个 BOM 提供版本约束"] --> B["Starter 声明直接和传递依赖"]
B --> C["Maven 按依赖管理与就近原则调解"]
C --> D["形成最终运行时 Classpath"]
D --> E{"接口版本与实现版本是否匹配"}
E -- "匹配" --> F["自动配置正常加载"]
E -- "不匹配" --> G["NoSuchMethodError、Bean 创建失败等"]这里故意不写“永远最新”的固定版本号。正确做法是先根据目标 JDK 和 Spring Boot 选择 Spring Cloud 发布列车,再对照 Spring Cloud Alibaba 官方版本说明选择兼容版本。
4.2 两条常见代际路线
| 路线 | 典型基础 | 适合场景 | 关键提醒 |
|---|---|---|---|
| 存量兼容路线 | JDK 8 + Spring Boot 2.x | 维护已有企业系统 | 先确认具体 Boot、Cloud、Alibaba 三方矩阵,不要单独升级 Starter |
| 当前演进路线 | Java 17+ + Spring Boot 3.x | 新项目或系统升级 | javax.* 已迁移到 jakarta.*,生态组件也必须支持 Boot 3 |
不能因为项目能编译就认定兼容。常见运行期错误包括 NoSuchMethodError、ClassNotFoundException、Bean 条件不匹配、配置加载阶段变化和客户端协议不匹配。
4.3 上线前版本核对清单
- 确定 JDK 大版本和供应商。
- 确定 Spring Boot 精确版本。
- 查 Spring Cloud 官方兼容关系,选择发布列车。
- 查 Spring Cloud Alibaba 官方版本说明。
- 核对 Nacos Client 与 Nacos Server 的兼容要求。
- 核对 Seata Client、TC Server、数据库脚本和序列化配置。
- 核对 RocketMQ Client/Binder 与 Broker 版本。
- 用
mvn dependency:tree检查是否被业务依赖覆盖。 - 在预发布执行注册、配置刷新、限流、消息重试和事务恢复测试。
五、最小依赖与边界清晰的配置
下面只展示依赖职责,不绑定某个可能过期的版本:
<dependencies>
<!-- Nacos 注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos 配置中心:按所选版本查看是否还需额外配置导入依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 声明式 HTTP 客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>原则是按需引入。只用 Nacos 注册发现的服务,不要因为“以后也许会用”就一次性引入 RocketMQ、Seata 和 Dubbo。
六、应用启动全过程
应用启动并不是“先注册然后读取配置”这么简单。配置可能影响数据源、端口、Bean 条件和注册元数据,因此远程配置必须在需要它的 Bean 创建之前进入环境。
flowchart TD
A["JVM 启动 SpringApplication"] --> B["建立 Environment 与配置加载上下文"]
B --> C["解析本地配置和远程 Config Data 导入"]
C --> D["Nacos Config 客户端定位并拉取配置"]
D --> E["远程 PropertySource 加入 Environment"]
E --> F["执行自动配置并创建业务 Bean"]
F --> G["创建 Feign 代理、LoadBalancer、Sentinel 适配器"]
G --> H["Web 容器启动并获得监听端口"]
H --> I["Nacos Discovery 注册实例与元数据"]
I --> J["订阅依赖服务并建立本地实例快照"]
J --> K["健康检查与 Readiness 通过后接收流量"]关键点:
- 远程配置加载失败是否允许启动,必须按配置重要性设计。数据库地址缺失通常应快速失败;非核心开关可使用安全默认值。
- “应用已注册”不一定代表“应用可提供服务”。数据库连接池、缓存预热、消息消费者等可能仍未就绪。
- 容器平台要把 Liveness 和 Readiness 分开。存活不代表可以接流量。
- 服务下线应先从流量入口摘除,等待在途请求结束,再停止容器。
七、Nacos Config:从配置定位到动态刷新
7.1 Namespace、Group、DataId
可以把配置定位理解成三层坐标:
Namespace:环境或租户隔离,例如 dev、test、prod
Group:业务域或配置分组,例如 ORDER_GROUP
DataId:具体配置标识,例如 order-service.yaml三者任何一个不同,都可能导致“控制台看得到,应用读不到”。生产环境不要依赖默认值,应明确配置并建立命名规范。
7.2 新旧配置加载方式
较新的 Spring Boot 配置机制使用 Config Data 导入,形式取决于所选版本,例如:
spring:
application:
name: order-service
config:
import:
- optional:nacos:order-service.yaml?group=ORDER_GROUP&refreshEnabled=trueoptional: 表示远程配置不可用时仍可能继续启动。核心配置不应不加分析地使用它,否则应用可能拿着默认数据库、默认超时或默认开关启动。
旧项目常见 bootstrap.yml、bootstrap context 或 spring-cloud-starter-bootstrap。它们属于特定版本时代的加载方式。迁移时必须验证配置优先级、profile、共享配置和刷新行为,不能只把文件名改成 application.yml。
7.3 动态刷新内部发生什么
flowchart TD
A["控制台发布新配置"] --> B["Nacos Server 保存版本并通知客户端"]
B --> C["客户端比较摘要并拉取最新内容"]
C --> D["更新 Environment 中的 PropertySource"]
D --> E{"Bean 是否支持重新绑定或重建"}
E -- "支持" --> F["新请求逐步读取新值"]
E -- "不支持" --> G["对象继续持有旧值,需显式重建或重启"]配置刷新不是跨线程、跨实例的全局原子事务:
- 不同实例收到通知有时间差。
- 一个请求执行过程中可能跨越配置切换时刻。
- 已创建的连接池、线程池、HTTP 客户端未必自动重建。
- 多个 DataId 的更新通常不是一个原子发布单元。
因此复杂规则应带版本号,代码要能兼容新旧版本短暂共存;高风险配置要灰度、审计、回滚。
7.4 客户端怎样发现配置变了
不同 Nacos 大版本的通信实现会演进,但稳定不变的逻辑是“监听目标配置、比较版本、收到变化后拉取正文、更新本地配置源”。客户端不会把每个业务线程都挂在控制台上等待:
- 应用根据 Namespace、Group、DataId 形成配置键。
- 配置客户端为这些键注册监听,并保留本地内容或摘要。
- 客户端与服务端通过对应版本支持的长轮询或长连接机制感知变化。
- 服务端只需要告诉客户端哪些配置可能变化;客户端再读取目标版本,避免把全部配置反复广播给所有应用。
- 客户端更新配置源并发布刷新事件,Spring 集成层再决定重新绑定哪些对象。
- 如果服务端暂时不可达,客户端可能读取本地缓存,但缓存只能用于容灾,不能证明它是最新配置。
“通知”和“配置正文”分开,是为了降低无效传输;“本地缓存”和“远程版本”并存,是为了可用性。但这也解释了为什么控制台刚发布后,不同实例可能短时间看到不同版本。
深入学习:Nacos 注册发现与配置中心 · 配置中心原理与排查
八、Nacos Discovery:注册、订阅和本地快照
8.1 注册阶段
提供者启动后向 Nacos 上报:服务名、IP、端口、Cluster、Group、权重、是否健康、是否启用和自定义元数据。注册成功后,还需要通过心跳或服务端探测维持健康状态,具体方式与实例类型及版本有关。
不要用一句“Nacos 是 AP”或“Nacos 是 CP”概括所有数据。Nacos 的数据模型、临时/持久实例、服务端版本和一致性实现存在差异。准确回答应先说明讨论的是哪类数据、哪种实例和哪个版本。
8.2 消费者发现阶段
flowchart TD
A["消费者订阅 inventory-service"] --> B["Nacos 返回当前实例列表"]
B --> C["客户端形成内存快照"]
C --> D["服务端实例变化通知客户端"]
D --> E["客户端更新快照"]
E --> F["LoadBalancer 从快照选择实例"]业务调用通常读取本地快照,不会每次请求都同步查询 Nacos。这样既降低注册中心压力,也避免 Nacos 短暂不可用直接阻断所有已有服务调用。代价是实例列表存在短暂陈旧窗口,所以调用端仍要设置超时、被动剔除、重试边界和熔断策略。
8.3 临时实例、持久实例和健康状态到底有什么不同
临时实例通常表达“进程活着才存在”的服务实例。客户端必须持续维持存活信息;长时间失联后,服务端可以将其移出可用列表并最终删除。持久实例表达“这个地址是被管理的固定对象”,健康检查失败时更倾向于保留实例记录并标记为不健康,等待恢复或人工管理。
这几个状态不能混为一谈:
| 状态 | 含义 | 调用方通常如何处理 |
|---|---|---|
| registered | 服务端保存了该实例记录 | 还不能据此直接调用 |
| healthy | 健康机制认为实例当前可用 | 才有资格进入候选集 |
| enabled | 运维或配置允许该实例接流量 | 禁用后即使健康也不应选择 |
| weight | 实例流量权重 | 权重为零常用于摘流或灰度 |
| metadata | 版本、机房、租户等路由标签 | 由调用方过滤器解释,不会自动产生所有灰度能力 |
实例刚宕机到所有消费者不再选择它之间存在传播窗口:健康判断需要时间,服务端状态变化需要传播,客户端快照需要替换,连接池还可能持有旧连接。这就是为什么必须同时设置连接/读取超时并做优雅下线。
8.4 Nacos 一致性应该怎样准确回答
不能背“Nacos 是 AP”这一句。应该先问:讨论的是注册数据还是配置数据、临时实例还是持久实例、哪个服务端版本和部署模式。配置发布需要可靠保存版本;服务发现更关注网络分区时能否继续提供已有实例快照;不同数据类型可以采用不同的一致性和健康管理路径。面试时若不限定对象,CP/AP 结论就缺少前提。
九、外部请求端到端调用全过程
假设用户调用“创建订单”,订单服务再调用库存服务:
flowchart TD
A["客户端请求并携带认证信息、幂等键"] --> B["Gateway 匹配 Route"]
B --> C["Gateway Filter 鉴权、限流、记录 Trace"]
C --> D["lb://order-service 触发服务发现"]
D --> E["LoadBalancer 从 Nacos 本地快照选实例"]
E --> F["转发到 Order Controller"]
F --> G["Order Service 调用 Feign 代理"]
G --> H["Sentinel 进入资源统计与规则链"]
H --> I["LoadBalancer 选择 inventory-service 实例"]
I --> J["HTTP Client 建连并发送请求"]
J --> K["Inventory Controller 与事务逻辑"]
K --> L["响应沿原链路返回并记录指标"]这里有两类平面:
- 控制面:Nacos 推送实例和配置、Sentinel 发布规则、控制台操作。
- 数据面:Gateway、Feign、HTTP 连接、业务服务真正传递请求。
控制面短暂故障时,数据面可能依靠缓存继续运行;但新实例发现、配置变更和规则下发会受影响。
十、OpenFeign 与 LoadBalancer 内部原理
10.1 Feign 代理如何产生
@EnableFeignClients 触发扫描,框架为 @FeignClient 接口注册工厂 Bean。业务 Bean 注入的不是手写实现,而是动态代理。调用接口方法时,代理会读取方法元数据,将 Java 参数编码为路径、查询参数、Header 或 Body,再交给拦截器、负载均衡客户端和底层 HTTP Client。
@FeignClient(name = "inventory-service", path = "/internal/inventory")
public interface InventoryClient {
@PostMapping("/reserve")
ReserveResponse reserve(@RequestBody ReserveRequest request,
@RequestHeader("X-Request-Id") String requestId);
}更细的对象链可以这样理解:
flowchart TD
A["FeignClientFactoryBean 创建客户端"] --> B["Contract 解析接口和方法注解"]
B --> C["为每个方法建立 MethodMetadata"]
C --> D["动态代理接收 Java 方法调用"]
D --> E["RequestTemplate 编码路径、参数、Header、Body"]
E --> F["RequestInterceptor 增加认证与 Trace"]
F --> G["LoadBalancer 解析服务名并选择实例"]
G --> H["底层 Client 复用连接并发送 HTTP"]
H --> I["Decoder 或 ErrorDecoder 处理响应"]如果参数注解写错,错误发生在“Java 参数编码”阶段;如果提示无实例,错误在服务发现或过滤阶段;连接超时发生在 TCP 建连阶段;读取超时表示通常已经连上,但在预算内没有读到完整响应。把这些阶段区分开,排查才不会只剩“Feign 调不通”。
10.2 LoadBalancer 做了什么
当 URL 使用逻辑服务名时,LoadBalancer 会:
- 向
ServiceInstanceListSupplier获取可用实例快照。 - 执行区域、版本、健康、权重等过滤策略。
- 用轮询、随机或自定义算法选择一个实例。
- 把逻辑主机名重建成真实
IP:Port。 - 交给 HTTP Client 建连、发送和读取响应。
“选到了实例”不等于调用成功。之后仍可能 DNS、连接池、握手、连接超时、读取超时、下游线程池拥塞或业务异常。
10.3 重试为什么危险
如果 Gateway 重试 2 次、Feign 重试 2 次、HTTP Client 再重试 2 次,最坏尝试次数可能乘法放大。写请求还会引入重复扣款、重复下单。正确做法是设定统一重试预算,只对明确可重试且幂等的错误重试,并使用指数退避和抖动。
深入学习:OpenFeign 远程调用 · 负载均衡全过程 · 服务调用端到端全过程
十一、Sentinel 调用原理与职责边界
Sentinel 保护的是“资源”。资源可以是 Web 路径、Feign 方法或显式包围的一段代码。请求进入资源后,会经过 Slot Chain:建立上下文、统计节点、检查授权/热点/流控/熔断等规则,最后决定通过还是抛出 BlockException。
flowchart TD
A["请求进入 Sentinel 资源"] --> B["建立 Context 与资源 Entry"]
B --> C["NodeSelector / ClusterNode 归集统计"]
C --> D["StatisticSlot 更新 QPS、并发、异常、RT"]
D --> E["Authority、Param、Flow、Degrade 等规则检查"]
E --> F{"是否违反治理规则"}
F -- "否" --> G["执行业务并记录结果"]
F -- "是" --> H["抛出 BlockException"]必须区分两类失败:
| 处理器 | 触发原因 | 示例 |
|---|---|---|
blockHandler | Sentinel 规则主动拦截 | 超过 QPS、热点参数限流、熔断器打开 |
fallback | 业务调用执行失败 | 下游超时、业务代码异常、HTTP 5xx |
降级返回只能表示“系统以可控方式失败”,不能伪装成业务成功。例如库存服务超时时,不能返回“扣减成功”;应返回明确的待确认状态或终止下单。
Sentinel Dashboard 默认推送到客户端内存的规则不适合作为唯一生产事实源。生产需按版本能力选择 Nacos 等数据源持久化规则,并验证重启、扩容和控制台修改后的行为。
11.1 滑动窗口不是“每秒一个计数器”
Sentinel 会把统计时间区间切成多个桶。请求到来时,根据当前时间定位桶,并在桶中累加通过数、阻塞数、异常数和耗时。计算最近窗口指标时汇总尚未过期的桶。
假设统计 1 秒并切成 2 个 500ms 桶:固定窗口可能在 999ms 和 1001ms 各放入一批流量而认为分属两秒;滑动窗口会同时观察最近 1 秒内的桶,边界突刺更容易被发现。桶越细统计越平滑,但对象、轮转和计算成本也更高。
11.2 熔断器为什么有半开状态
熔断后若直接恢复全部流量,下游尚未恢复就会再次被压垮。半开状态只允许少量探测请求:成功达到条件才关闭熔断器,失败则重新打开。熔断器保护的是调用资源和上游线程,不会撤销已执行的下游操作,所以写请求超时后仍需查询事实状态和保证幂等。
深入学习:Sentinel 资源、SlotChain 与滑动窗口
十二、Gateway 在 Alibaba 技术栈中的位置
Gateway 属于 Spring Cloud,不是 Nacos 的子组件,但它可以使用 Nacos 的服务实例列表并通过 lb://service-name 转发。
一次 Gateway 请求会先由路由谓词判断是否匹配,再把全局过滤器与路由过滤器按顺序组合成过滤链。过滤器执行具有“前置逻辑向下调用、后置逻辑在返回时逆序执行”的特征。真正转发阶段根据 URI scheme 选择对应过滤器;lb:// 会先选择服务实例并重写目标地址,随后 Netty HTTP Client 才建立或复用连接。
flowchart TD
A["RoutePredicateHandlerMapping 匹配路由"] --> B["FilteringWebHandler 组合并排序过滤器"]
B --> C["前置过滤:认证、限流、Header、Trace"]
C --> D["LoadBalancer 处理 lb:// 服务名"]
D --> E["路由过滤器发出下游请求"]
E --> F["写回响应"]
F --> G["过滤器后置逻辑逆序完成"]Gateway 基于 Reactor 异步链路。过滤器里执行长时间阻塞数据库查询或调用 .block(),会占住少量事件循环线程,表现为整个网关大量请求一起变慢,而不只是当前请求变慢。
正确职责:
- TLS 终止、统一认证、CORS、Header 清洗。
- 路由匹配、灰度标记、请求大小限制。
- 粗粒度限流、超时和访问日志。
- Trace 上下文创建或透传。
不适合放入 Gateway 的职责:
- 长事务和复杂订单编排。
- 直接访问多个业务数据库。
- 把所有下游响应统一改成成功。
- 无边界重试非幂等写请求。
深入学习:Spring Cloud Gateway 路由与过滤链
十三、RocketMQ 与 Spring Cloud Stream
Spring Cloud Stream 提供 Binder、Binding 和函数式编程模型;RocketMQ Binder 把这些抽象映射到 RocketMQ。抽象减少样板代码,但不会消除底层语义差异。
flowchart TD
A["业务函数产生事件"] --> B["Stream Binding"]
B --> C["RocketMQ Binder 转换消息"]
C --> D["Producer 发送到 Broker"]
D --> E["Broker 持久化并返回结果"]
E --> F["Consumer 拉取或接收消息"]
F --> G["执行业务与幂等检查"]
G --> H["成功确认或失败重试、死信"]必须继续设计:
- 生产者发送失败和不确定结果如何处理。
- Topic、Tag、Consumer Group 如何规划。
- 消费失败重试次数和死信队列。
- 重复投递如何通过业务唯一键幂等。
- 消息积压如何按 Queue Lag 定位瓶颈。
- 事务消息只保证本地事务与“是否投递消息”的协调,不保证消费者业务恰好执行一次。
13.1 Binder 到 RocketMQ 的映射过程
Supplier、Function、ConsumerBean 描述应用的输入输出函数。- Binding 名把函数端口与逻辑 destination 关联。
- RocketMQ Binder 创建生产者或消费者客户端,并将 destination、group、Tag 等映射到底层 RocketMQ 概念。
- 发送时先做消息转换和 Header 映射,再调用 RocketMQ Producer。
- 消费时由客户端取得消息,Binder 转换为函数参数并调用业务 Consumer。
- 函数正常返回后按绑定模式确认;抛出异常会进入重试、重新投递或死信路径,行为必须结合版本和配置验证。
最小函数式消费者:
@Bean
public Consumer<OrderCreatedEvent> orderCreated() {
return new Consumer<OrderCreatedEvent>() {
@Override
public void accept(OrderCreatedEvent event) {
// 先按 eventId 做幂等占位,再在同一事务中修改业务状态
orderApplicationService.handleCreated(event);
}
};
}spring:
cloud:
function:
definition: orderCreated
stream:
bindings:
orderCreated-in-0:
destination: order-created
group: fulfillment-service函数名、Binding 名、destination 和 consumer group 是四个不同层次。函数名不等于 Topic;同一 group 内实例竞争消费,不同 group 各自获得一份订阅语义。
深入学习:Spring Cloud Stream 与 Bus · RocketMQ 专栏 · 消息堆积与背压 · 消费幂等原理
十四、Seata:分布式事务不是加一个注解就结束
Seata 常见角色:
- TC:事务协调者,维护全局事务状态。
- TM:事务管理者,开启、提交或回滚全局事务。
- RM:资源管理者,注册分支事务并操作数据库等资源。
| 模式 | 核心思路 | 优点 | 主要代价 |
|---|---|---|---|
| AT | 代理数据源并记录前后镜像,通过 undo_log 补偿 | 业务改造相对小 | SQL 支持、全局锁、热点冲突、回滚数据正确性 |
| XA | 使用数据库 XA 能力准备和提交 | 一致性语义明确 | 持锁时间和数据库/驱动能力约束 |
| TCC | 业务显式实现 Try、Confirm、Cancel | 控制力强 | 侵入大,必须处理幂等、空回滚和悬挂 |
| Saga | 长事务拆成步骤及补偿 | 适合长流程 | 补偿不等于物理回滚,业务复杂度高 |
14.1 AT 模式为什么需要前后镜像和全局锁
以订单服务执行 UPDATE account SET balance = balance - 100 WHERE id = 1 为例:
flowchart TD
A["TM 向 TC 开启全局事务并获得 XID"] --> B["XID 随远程调用传播"]
B --> C["RM 代理数据源执行更新"]
C --> D["解析 SQL 并查询更新前镜像"]
D --> E["执行真实 SQL 并查询更新后镜像"]
E --> F["业务数据与 undo_log 在本地事务提交"]
F --> G["RM 向 TC 注册分支并持有全局锁语义"]
G --> H{"全局事务最终结果"}
H -- "提交" --> I["异步清理 undo_log"]
H -- "回滚" --> J["校验当前数据与后镜像"]
J --> K["根据前镜像生成补偿并恢复数据"]前镜像用于知道“原来是什么”,后镜像用于回滚前判断数据是否已被其他逻辑改变。若当前值不再等于后镜像,直接覆盖成前镜像可能抹掉别人的更新,因此会出现脏写检查或需要人工介入。undo_log 与业务 SQL 必须在同一本地事务提交,否则可能发生业务已提交却没有可回滚日志。
全局锁不是一直持有数据库本地行锁。第一阶段本地事务提交后数据库行锁释放,但 Seata 通过协调信息限制其他全局事务对相同资源的冲突操作。普通不经过 Seata 代理的数据更新可能绕过这层语义,所以 AT 不是“对任何写入都自动安全”。
调用超时后,上游不知道下游是否已经提交,这是分布式系统的客观不确定性。Seata 不能替代幂等键、状态机、对账、补偿任务和人工兜底。
深入学习:Seata 四种模式 · 分布式事务总览 · TCC/Saga 步骤幂等
十五、Dubbo 与 Spring Cloud Alibaba 的关系
OpenFeign 常用于声明式 HTTP 调用;Dubbo 是 RPC 框架,拥有服务接口代理、协议、序列化、Invoker、Cluster、Router、LoadBalance、Filter 等完整调用体系。二者不是简单的“新旧替代”关系。
Dubbo 消费调用的核心链路是:接口代理把 Java 调用转成 Invocation;Cluster Invoker 获取目录中的多个 Provider Invoker;Router 过滤候选实例;LoadBalance 选一个;Filter 链处理上下文、超时、监控和异常;协议层编码请求并写入连接;响应根据 requestId 与调用方 Future 对应;提供者解码后在线程模型允许的执行器中调用真实实现。
flowchart TD
A["接口 Proxy"] --> B["Invocation"]
B --> C["Cluster Invoker 与容错策略"]
C --> D["Directory 获取地址并由 Router 过滤"]
D --> E["LoadBalance 选择 Provider Invoker"]
E --> F["Filter 链"]
F --> G["协议编码、连接与 RequestId"]
G --> H["Provider 解码、线程派发、调用实现"]
H --> I["响应匹配 Future 并反序列化"]重试发生在 Cluster 容错层时,前一次请求可能已经到达提供者,只是响应丢失。因此 Dubbo 写接口也必须幂等;不能因为客户端最终收到了第二次成功响应,就认为第一次一定没有执行。
适合考虑 Dubbo 的情况:内部 Java 服务调用密集、接口契约稳定、对 RPC 性能和治理扩展点有明确需求。适合 HTTP/OpenFeign 的情况:跨语言、开放 API、调试可见性和通用协议优先。
一个系统可以在边界使用 HTTP,在内部特定高频链路使用 Dubbo,但必须避免同一业务能力同时暴露多套语义不一致的接口。
深入学习:Dubbo 调用链、协议与 SPI
十六、可观测性:一次调用必须留下哪些证据
Spring Cloud Alibaba 不会自动让所有故障可见。生产系统至少需要:
| 类型 | 要回答的问题 | 关键字段 |
|---|---|---|
| Metrics | 影响多大、从何时开始 | QPS、成功率、P95/P99、线程池、连接池、Lag、熔断状态 |
| Traces | 慢在哪一跳、错误在哪一跳 | traceId、spanId、服务、实例、路由、错误标签 |
| Logs | 当时发生了什么 | traceId、业务键、错误码、耗时、目标实例、重试次数 |
日志不能输出密码、Token、完整身份证、密钥和敏感医疗数据。业务键也应按安全规范脱敏。
十七、商业订单场景的生产架构
flowchart TD
A["用户提交订单与幂等键"] --> B["Gateway 认证、限流、路由"]
B --> C["订单服务本地事务创建待处理订单"]
C --> D["调用库存服务预占库存"]
D --> E["调用支付或创建支付单"]
E --> F["事务消息发布订单事件"]
F --> G["履约、通知、搜索索引异步消费"]
G --> H["对账与补偿任务扫描异常状态"]这里每个组件都只承担明确职责:
- Nacos:提供服务地址和配置,不转发订单请求。
- Gateway:入口治理,不直接修改订单表。
- Feign/LoadBalancer:选择并调用库存服务。
- Sentinel:控制过载和故障扩散。
- RocketMQ:异步传播已经发生的业务事实。
- Seata 或业务 Saga:仅在明确需要时协调一致性。
- 幂等表与状态机:处理重复请求、重复消息和结果不确定。
- 对账补偿:处理在线链路无法自动确认的异常。
十八、生产高可用与安全清单
Nacos
- 使用集群和持久化存储,避免单节点成为控制面单点。
- dev、test、prod 使用独立 Namespace;高安全要求时使用独立集群。
- 注册元数据和配置权限最小化,开启认证并定期轮换凭据。
- 配置发布经过审批、灰度、审计和回滚。
- 客户端缓存不能成为永久依赖,要监控与服务端失联时长。
Sentinel
- 规则持久化到可靠数据源。
- 阈值来自容量测试,不是凭感觉填写。
- 同时监控被拒绝数、异常率、RT 和降级结果。
- 明确系统规则、流控规则、热点规则和熔断规则的生效范围。
RocketMQ
- Broker、NameServer、磁盘和副本策略按业务可靠性设计。
- 监控发送失败、消费失败、重试、死信和各 Queue Lag。
- 消费者必须幂等;扩容前先确认队列并行度和下游容量。
Seata
- TC 集群、事务日志、注册配置中心和数据库脚本必须匹配。
- AT 模式检查
undo_log、全局锁冲突、长事务和热点行。 - 明确事务超时、回滚失败、悬挂事务的恢复与告警。
十九、经典组件迁移地图
| 经典方案 | 当前常见方向 | 迁移时真正要验证的内容 |
|---|---|---|
| Eureka | Nacos、Consul、Kubernetes Service,或继续维护 Eureka | 注册语义、健康检查、本地缓存、元数据、下线时延 |
| Ribbon | Spring Cloud LoadBalancer | 算法、自定义规则、重试绑定、实例过滤、灰度路由 |
| Hystrix | Sentinel 或 Resilience4j | 隔离方式、熔断指标、fallback 语义、监控面板 |
| Zuul 1.x | Spring Cloud Gateway | Servlet 到 WebFlux 的线程模型、Filter 顺序、Body 读取、超时 |
| Sleuth | Micrometer Tracing / OpenTelemetry | Trace 传播格式、采样、Exporter、日志关联 |
bootstrap.yml 老配置链 | Config Data import | 加载时机、优先级、profile、共享配置、失败策略 |
迁移不能只替换 Maven 坐标。必须用故障注入验证注册中心不可用、下游超时、配置刷新、熔断恢复、消息重复和实例优雅下线。
二十、生产故障排查 Runbook
20.1 服务名找不到实例
- 查消费者日志中的 Namespace、Group、服务名是否与提供者一致。
- 在 Nacos 控制台确认实例是否存在、健康、启用、端口正确。
- 查提供者是否注册了容器不可达 IP。
- 查消费者本地实例快照是否更新,而不是只看控制台。
- 查网络、认证、ACL、Nacos Client/Server 兼容性。
- 如果只有部分请求失败,按目标实例维度统计,找出坏节点。
20.2 配置存在但应用没生效
- 打印脱敏后的应用名、profile、Namespace、Group、DataId。
- 查启动日志是否真正拉到目标配置,是否用了
optional:静默降级。 - 查 PropertySource 优先级,确认是否被环境变量或本地配置覆盖。
- 查目标 Bean 是否支持刷新,是否在构造时缓存了旧值。
- 对比不同实例配置版本,判断是否为推送延迟或部分失败。
- 高风险配置回滚后再分析,不要在线反复修改制造更多变量。
20.3 调用 RT 突然升高
flowchart TD
A["按 Trace 确定慢在哪一跳"] --> B["区分连接等待、连接耗时、服务处理、响应读取"]
B --> C["按目标实例观察 P95/P99 与错误率"]
C --> D["检查线程池、连接池、GC、CPU、下游数据库"]
D --> E["检查重试放大、Sentinel 规则和熔断状态"]
E --> F["限流止损、摘除坏实例或降级非核心功能"]20.4 Sentinel 大量 Block
先判断是流控、热点、授权还是熔断规则;再看阈值、统计窗口、最小请求数、实际容量和下游健康。不要直接把阈值调大,否则可能把“可控拒绝”变成线程池耗尽和全链路雪崩。
20.5 Seata 事务长时间不结束
查 XID、全局事务状态、各分支状态、TC 日志、业务服务日志、数据库锁等待和 undo_log。先确认是在提交、回滚还是重试阶段,再决定人工补偿;不要直接删除事务记录或 undo_log。
二十一、JDK 8 原理 Demo:注册表快照与本地负载均衡
这个 Demo 不伪装成 Nacos 客户端,而是用最小代码证明关键原理:注册中心返回服务实例,消费者保存不可变快照,每次请求由本地 LoadBalancer 选择实例。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class DiscoverySnapshotDemo {
static final class Instance {
private final String host;
private final int port;
Instance(String host, int port) {
this.host = host;
this.port = port;
}
String address() {
return host + ":" + port;
}
}
static final class SnapshotLoadBalancer {
private volatile List<Instance> snapshot = Collections.emptyList();
private final AtomicInteger position = new AtomicInteger();
void onRegistryChanged(List<Instance> instances) {
snapshot = Collections.unmodifiableList(
new ArrayList<Instance>(instances));
}
Instance choose() {
List<Instance> current = snapshot;
if (current.isEmpty()) {
throw new IllegalStateException("no available instance");
}
int index = Math.floorMod(position.getAndIncrement(), current.size());
return current.get(index);
}
}
public static void main(String[] args) {
List<Instance> registryResult = new ArrayList<Instance>();
registryResult.add(new Instance("10.0.0.11", 8080));
registryResult.add(new Instance("10.0.0.12", 8080));
SnapshotLoadBalancer lb = new SnapshotLoadBalancer();
lb.onRegistryChanged(registryResult);
System.out.println(lb.choose().address());
System.out.println(lb.choose().address());
System.out.println(lb.choose().address());
}
}JDK 8 输出:
10.0.0.11:8080
10.0.0.12:8080
10.0.0.11:8080volatile 保证其他请求线程能看到新快照,复制并包装不可变列表避免更新线程与选择线程并发修改同一个集合。真实客户端还会增加订阅、健康过滤、缓存持久化、权重、集群偏好等机制。
二十二、常见错误与“不这样会怎样”
| 错误做法 | 后果 | 正确方向 |
|---|---|---|
| 所有 Starter 一次性引入 | 自动配置冲突、启动变慢、版本难治理 | 按能力最小引入 |
| 每次业务调用都查询 Nacos | 注册中心被业务流量压垮,故障耦合 | 订阅并读取本地快照 |
| 所有配置都允许动态刷新 | 对象状态不一致、连接资源泄漏 | 配置分级,必要时滚动重启 |
| 写请求层层自动重试 | 重复订单、重试风暴 | 统一预算、幂等键、退避抖动 |
| fallback 永远返回成功 | 数据已失败但用户收到成功 | 返回明确降级状态并补偿 |
| Sentinel 规则只存在控制台内存 | 重启或扩容后规则丢失 | 持久化并纳入发布流程 |
| 使用 Seata 后不做幂等和对账 | 超时、重试、回滚失败无法收口 | 状态机、幂等、补偿、告警 |
| Stream 屏蔽了 MQ 就不学 Broker | 不会处理 ACK、Lag、死信和顺序 | 理解抽象,也理解底层语义 |
二十三、面试标准回答
23.1 Spring Cloud Alibaba 是什么
Spring Cloud Alibaba 是 Alibaba 生态与 Spring Cloud 编程模型的集成体系。常用能力包括 Nacos 注册发现和配置、Sentinel 流量治理、RocketMQ Stream Binder、Seata 分布式事务集成,并可与 OpenFeign、LoadBalancer、Gateway、Dubbo 和可观测性体系组合。它不是单个服务,也不会自动解决所有微服务问题。
23.2 一次 Feign 调用是否每次都经过 Nacos
通常不会。消费者先从 Nacos 获取并订阅服务实例,维护本地快照;调用时由 LoadBalancer 从快照选实例,再由 HTTP Client 直连提供者。Nacos 属于控制面,Feign 到提供者的 HTTP 请求属于数据面。
23.3 Nacos 配置刷新为什么可能不生效
配置中心更新的是配置源,但具体 Bean 是否读取新值取决于配置绑定和刷新机制。构造时缓存值、连接池、线程池及第三方客户端可能继续使用旧配置,而且多实例接收更新存在时间差,因此动态刷新不是全局原子更新。
23.4 Sentinel、Gateway 限流和线程池隔离怎么选
Gateway 适合入口粗粒度流量治理;Sentinel 可保护具体 Web、Feign 或业务资源,并基于 QPS、并发、热点和熔断指标做规则判断;线程池/信号量隔离限制资源占用。生产常分层组合,但必须避免多个阈值互相冲突。
23.5 Seata 和 RocketMQ 事务消息能否保证消费者只执行一次
不能。Seata 协调受控分支事务;RocketMQ 事务消息协调本地事务与消息是否可投递。网络超时和消费确认丢失仍可能导致重复投递,消费者必须基于业务唯一键实现幂等,并准备对账和补偿。
二十四、推荐学习路线
- 先学微服务与 Spring Cloud 组件全景,理解控制面、数据面和分布式失败。
- 再学本页,建立 Spring Cloud Alibaba 总装关系和版本边界。
- 学Nacos与配置中心,掌握控制面。
- 学OpenFeign与LoadBalancer,掌握内部调用。
- 学Gateway与Sentinel,掌握入口和稳定性。
- 学可观测性,否则线上无法证明问题在哪。
- 再学RocketMQ、分布式幂等和Seata,掌握异步和一致性。
- 最后学习经典组件迁移和Spring Cloud 面试页,形成新旧技术对比。
本章小结
真正掌握 Spring Cloud Alibaba,不是能写出几个 Starter,而是能画出配置加载、服务注册、服务发现、负载选择、请求发送、资源保护、消息投递和事务恢复的完整链路;还要清楚每个组件在哪个失败窗口会失效,以及由幂等、状态机、补偿和可观测性如何收口。
