Spring 全栈硬核教程 · 06:Spring Cloud(微服务与云原生)
这一篇有两个反直觉的核心观点,先放在最前面:
① 大部分团队不需要微服务。② 上了微服务的团队里,大部分不需要分布式事务。
如果你读完这篇只记住这两句,这篇就值了——因为微服务架构里绝大多数的痛苦,都来自"不该拆的拆了"和"不该用的用了"。
1. 先问该不该拆:微服务的真实成本账
1.1 单体不是原罪
| 维度 | 单体 | 微服务 |
|---|---|---|
| 开发调试 | 一个 IDE 跑通全流程 | 要起 5 个服务才能跑通一个下单 |
| 部署 | 一个包 | N 个包 + 编排 + 版本对齐 |
| 事务 | 本地事务,数据库保证 | 分布式事务,代价巨大 |
| 调用 | 方法调用,纳秒级,不会失败 | 网络调用,毫秒级,随时可能超时/失败 |
| 排查问题 | 一个堆栈看到底 | 要链路追踪才能拼出全貌 |
| 团队协作 | 发布窗口互相阻塞 | ✅ 独立发布,这是微服务的核心价值 |
| 弹性伸缩 | 整体扩容 | ✅ 按模块精准扩容 |
| 技术栈 | 统一 | ✅ 可异构(AI 用 Python,交易用 Java) |
1.2 什么时候才该拆
该拆的信号(满足 2 条以上再考虑):
- 团队超过 2 个 pizza team(约 15-20 人),发布互相阻塞已成常态
- 某个模块的资源需求与其他模块差异巨大(秒杀要 50 个实例,后台管理 2 个就够)
- 某个模块需要不同的技术栈(AI 推理服务)
- 某个模块的变更频率远高于其他部分(营销活动一天改三次,核心账务一季度改一次)
不该拆的信号:
- "微服务是趋势" ← 这不是理由
- 团队只有 5 个人 ← 你会花 60% 的精力在运维上
- 业务边界还没想清楚 ← 拆错的边界比不拆更痛苦,因为改边界要跨服务重构
务实路径:模块化单体(Modular Monolith)。用 Spring Modulith(07 篇详述)在单体内部强制模块边界,享受清晰架构的同时避免分布式的全部代价。等边界被业务验证稳定了、团队规模真的顶不住了,再把模块提取成服务——这时候拆的成本反而最低。
2. Spring Cloud 版本矩阵(这是最大的坑)
| Spring Cloud 列车 | 代号 | 对应 Spring Boot |
|---|---|---|
| 2025.1.x | Oakwood | 基于 Boot 4.0.x,兼容 4.1.x(当前主线) |
| 2025.0.x | —— | ⚠️ 与 Boot 4.0.1+ 不兼容,必须升到 2025.1.x |
| 2026.0.0 | Paddington | 对应 Boot 4.2(开发中) |
<properties>
<spring-cloud.version>2025.1.2</spring-cloud.version>
</properties>
<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>
</dependencies>
</dependencyManagement>铁律:永远先查官方兼容矩阵再选版本号。Spring Cloud 的版本号和 Boot 的版本号看起来毫无关联,凭感觉选的结果通常是一堆
NoSuchMethodError—— 而且这类错误的报错信息极具误导性,会让你怀疑人生。
3. 组件选型:Netflix 系 vs Alibaba 系
| 能力 | Spring Cloud 原生 | Spring Cloud Alibaba | 建议 |
|---|---|---|---|
| 注册发现 | Eureka(Netflix 已停止大版本更新)、Consul、Zookeeper | Nacos | 国内 → Nacos;K8s 环境 → 直接用 K8s Service |
| 配置中心 | Spring Cloud Config(依赖 Git) | Nacos Config、Apollo | Nacos(和注册中心合一)或 Apollo(权限审计更强) |
| 服务调用 | OpenFeign / HTTP Interface | 同左,或 Dubbo RPC | 跨语言→HTTP;纯 Java 高频→Dubbo |
| 网关 | Spring Cloud Gateway | 同左 | Gateway(基于 WebFlux,非阻塞,性能好) |
| 熔断限流 | Resilience4j(Hystrix 已停维护) | Sentinel | 国内 → Sentinel(有控制台,规则可视化配置) |
| 分布式事务 | 无原生方案 | Seata | 见第 6 节(先问该不该用) |
| 链路追踪 | Micrometer Tracing(Sleuth 已废弃) | 同左 + SkyWalking | SkyWalking(无侵入探针)或 OTel |
国内现实:Netflix OSS 系列(Eureka、Hystrix、Zuul、Ribbon)大部分已停止维护,国内主流是 Nacos + Sentinel + Gateway + Seata 这套组合。学习时把精力放在 Alibaba 系性价比更高,但原理是相通的——理解了服务发现的本质,换哪个注册中心都是一天上手的事。
4. 服务注册与发现:原理比 API 重要
服务启动 → 向注册中心注册(IP:Port + 元数据)
↓
注册中心维护服务列表
↓
消费者拉取列表(定时拉 + 推送变更)→ 本地缓存
↓
负载均衡选一个实例 → 发起调用
↓
心跳续约维持健康;心跳超时 → 实例被摘除三个必须理解的机制:
- 本地缓存兜底:消费者会把服务列表缓存在本地。注册中心挂了,已有的调用不受影响——这是注册中心可以"不那么高可用"的原因。但新服务上线、扩缩容会失效。
- AP vs CP:Nacos 默认 AP(保可用,允许短暂的列表不一致),Zookeeper 是 CP(保一致,选主期间不可用)。服务发现场景绝大多数应该选 AP——宁可短暂读到一个已下线的实例(调用失败重试即可),也不能因为注册中心选主而让整个集群不可用。
- 优雅下线:实例停机前应该先从注册中心注销、等待流量摘除、再关闭进程,否则会有一段时间的请求打到已死的实例上。
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
server:
shutdown: graceful # ⭐ 优雅停机:等已有请求处理完再退出这两行配置是"发布时用户报错"的解药。很多团队每次发版都有几十个 5xx,根因就是没开优雅停机——Pod 被 kill 时正在处理的请求直接断掉。
5. 服务治理:熔断、限流、降级
5.1 三个概念别搞混
| 概念 | 解决什么 | 类比 |
|---|---|---|
| 限流 | 保护自己不被打垮(入口流量太大) | 景区限制每日入园人数 |
| 熔断 | 保护自己不被下游拖垮(下游挂了,快速失败别等) | 保险丝烧断,防止整栋楼着火 |
| 降级 | 核心功能保命,非核心功能先关掉 | 断电时优先保证手术室供电 |
5.2 熔断器状态机(面试必考)
失败率超过阈值
CLOSED ──────────────→ OPEN
↑ │ 等待时间窗口结束
│ ↓
└───── 探测成功 ── HALF_OPEN ──探测失败──→ OPEN- CLOSED:正常放行,统计失败率
- OPEN:直接快速失败,不再调用下游(给下游喘息时间,这是关键)
- HALF_OPEN:放少量请求试探,成功就恢复,失败就继续熔断
熔断最重要的价值不是"保护自己",是"保护下游"。下游已经过载了,你还在不停重试,等于持续给它加压,它永远恢复不了。快速失败让下游有机会缓过来——这是雪崩防护的核心逻辑。
5.3 Sentinel 实战
@SentinelResource(value = "getOrder", blockHandler = "handleBlock", fallback = "handleFallback")
public OrderDTO getOrder(Long id) {
return orderClient.get(id);
}
// blockHandler:被限流/熔断时调用(规则触发)
public OrderDTO handleBlock(Long id, BlockException ex) {
return OrderDTO.empty();
}
// fallback:业务异常时调用(代码抛异常)
public OrderDTO handleFallback(Long id, Throwable ex) {
return cacheService.getLastKnown(id); // 降级返回缓存中的旧数据
}
blockHandler和fallback的区别是高频面试点:前者处理规则触发(限流/熔断/热点参数),后者处理业务异常。两者都配时,blockHandler优先级更高。
5.4 重试的危险性
@Retryable(maxAttempts = 3, delay = 1000, multiplier = 2, jitter = 200)
public PaymentResult charge(Order order) { ... }⚠️ 重试是把双刃剑:下游过载时,重试会让流量成倍放大,把"有点慢"变成"彻底挂"。三条铁律:
- 只对幂等操作重试——对非幂等的支付接口重试 3 次可能扣 3 次钱
- 必须用指数退避 + 随机抖动(
multiplier+jitter),否则所有客户端会同时重试,形成重试风暴- 重试必须和熔断配合——熔断打开时不再重试
6. ⭐ 分布式事务:先问该不该用
6.1 四种方案的真实成本
| 方案 | 一致性 | 性能 | 侵入性 | 复杂度 | 适用 |
|---|---|---|---|---|---|
| 2PC / XA | 强一致 | ❌ 差(全程锁资源) | 低 | 中 | 传统金融,低并发 |
| TCC | 最终一致 | 好 | ❌ 极高(每个服务写 Try/Confirm/Cancel 三个方法) | 高 | 资金类核心链路 |
| Saga | 最终一致 | 好 | 中(要写补偿逻辑) | 中 | 长流程业务(订单履约) |
| 本地消息表 / Outbox | 最终一致 | 好 | 低 | 低 | ⭐ 绝大多数场景的最优解 |
6.2 为什么大部分场景不需要分布式事务
先考虑这三个替代方案,成本低一个数量级:
① 重新审视服务边界:如果 A 和 B 总是要放在一个事务里,说明它们本来就不该拆成两个服务。这不是技术问题,是领域建模问题。合并回去,本地事务解决一切。
② 最终一致就够了:"下单成功但积分晚 3 秒到账"在业务上完全可以接受。不要把技术上的强一致执念,强加到本来能容忍延迟的业务上。
③ 本地消息表(事务性发件箱):
@Transactional // ⭐ 业务数据 + 消息记录写在同一个本地事务里,天然原子
public void createOrder(CreateOrderCmd cmd) {
Order order = orderRepository.save(toOrder(cmd));
outboxRepository.save(new OutboxMessage("ORDER_CREATED", order.getId(), toJson(order)));
}
// 独立的投递任务:扫表 → 发 MQ → 标记已发送(失败自动重试)
@Scheduled(fixedDelay = 1000)
public void dispatch() {
outboxRepository.findUnsent(100).forEach(msg -> {
mqProducer.send(msg);
outboxRepository.markSent(msg.getId());
});
}这个方案为什么好:没有引入任何新中间件,靠数据库的本地事务就保证了"业务数据写成功 ⟺ 消息一定会被发出去",配合消费端幂等就实现了完整的最终一致性。投入产出比远超 Seata/TCC。
给技术负责人的建议:当有人提议引入分布式事务框架时,先让他回答——"能不能调整服务边界?""业务真的不能接受几秒延迟吗?""本地消息表为什么不行?"三个问题都答不上来,再考虑 Seata。
7. 网关:流量的第一道关
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("order-service", r -> r
.path("/api/orders/**")
.filters(f -> f
.stripPrefix(1)
.circuitBreaker(c -> c.setName("orderCB").setFallbackUri("forward:/fallback"))
.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))
.retry(cfg -> cfg.setRetries(2).setMethods(HttpMethod.GET)) // ⚠️ 只对 GET 重试
)
.uri("lb://order-service"))
.build();
}网关该做什么 / 不该做什么:
| 该做 ✅ | 不该做 ❌ |
|---|---|
| 路由转发、负载均衡 | 复杂业务逻辑(网关变成新的单体) |
| 统一认证(校验 Token) | 业务鉴权(细粒度权限应在服务内) |
| 限流、熔断、灰度路由 | 数据聚合(这是 BFF 层的活) |
| 日志、链路 ID 注入 | 耗时的同步阻塞操作(网关是 WebFlux,阻塞会拖垮事件循环) |
网关是 WebFlux/Netty 的,千万别在自定义 Filter 里写阻塞代码(比如同步查数据库)。Netty 的事件循环线程数量很少(默认约等于 CPU 核数),阻塞一个就少一个,几个慢请求就能把整个网关打瘫。这是网关最经典的自杀方式。
8. 可观测性:分布式系统的眼睛
8.1 三大支柱
| 支柱 | 回答什么 | 方案 |
|---|---|---|
| Logs | 发生了什么细节 | Logback → JSON → ELK / Loki |
| Metrics | 系统整体健康度、趋势 | Micrometer → Prometheus → Grafana |
| Traces | 一个请求跨了哪些服务,每跳多久 | Micrometer Tracing → Zipkin / SkyWalking / OTel |
8.2 让日志和链路打通(最实用的一招)
<!-- logback-spring.xml,把 traceId 打进每一行日志 -->
<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n</pattern>management:
tracing:
sampling:
probability: 0.1 # ⚠️ 生产别用 1.0,追踪数据量会压垮存储这一招的价值:用户报障 → 从错误响应里拿到 traceId(回顾 03 篇把 traceId 放进 ProblemDetail 的做法)→ 日志系统按 traceId 一搜 → 全链路所有服务的日志按时间顺序排好呈现在你面前。排查时间从"半天"降到"两分钟"。这是分布式系统里投入产出比最高的基础设施建设。
8.3 采样率的权衡
100% 采样会产生海量数据(存储成本 + 性能开销)。生产实践:
- 常规流量 1%-10% 采样
- 错误请求 100% 采样(尾部采样 tail-based sampling,出错的一定留下)
- 关键链路(支付)单独提高采样率
9. 服务网格与 K8s:Spring Cloud 会被取代吗
| 能力 | Spring Cloud | K8s 原生 / Service Mesh |
|---|---|---|
| 服务发现 | Nacos/Eureka | K8s Service + DNS |
| 配置管理 | Nacos/Apollo | ConfigMap / Secret |
| 负载均衡 | 客户端 LB(Spring Cloud LoadBalancer) | kube-proxy / Envoy Sidecar |
| 熔断限流 | Sentinel(应用内) | Istio(基础设施层,业务代码零感知) |
| 链路追踪 | Micrometer Tracing | Sidecar 自动注入 |
趋势判断:基础设施能力(服务发现、负载均衡、重试、熔断)正在从应用层下沉到平台层。Service Mesh 的优势是业务代码零侵入、跨语言统一治理;代价是 Sidecar 的资源开销和运维复杂度。
务实结论:两者不是取代关系,而是分层关系。 2026 年的典型架构是——K8s 负责部署、发现、扩缩容;Mesh 或网关负责流量治理;Spring Cloud 的组件按需保留(比如配置中心的动态推送和灰度能力,K8s ConfigMap 目前做不到那么细)。别指望一把梭,也别全都上。
10. 本篇自测
- 微服务的核心价值是什么?什么信号出现时才该拆?
- Spring Cloud 2025.0.0 和 Boot 4.0.1 为什么不兼容?选版本的正确姿势?
- 服务发现为什么应该选 AP 而不是 CP?
- 优雅停机解决了什么问题?两行配置分别是什么?
- 熔断器的三个状态怎么流转?熔断最重要的价值是保护谁?
- Sentinel 的
blockHandler和fallback有什么区别? - 重试为什么危险?三条铁律是什么?
- 分布式事务的四种方案成本对比?为什么说本地消息表是多数场景最优解?
- 为什么网关的自定义 Filter 里绝不能写阻塞代码?
- traceId 打进日志为什么是投入产出比最高的基建?
下一篇(07 · 前沿与扩展):Spring AI 怎么让 Java 后端接上大模型(含 MCP 协议)、虚拟线程 vs Goroutine 的调度模型对比、GraalVM 原生镜像的真实收益与代价、Spring Modulith 如何拯救单体、Spring Batch 处理千万级数据。2026 年该把学习时间投在哪里,这一篇给答案。