Skip to content

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.xOakwood基于 Boot 4.0.x,兼容 4.1.x(当前主线)
2025.0.x——⚠️ 与 Boot 4.0.1+ 不兼容,必须升到 2025.1.x
2026.0.0Paddington对应 Boot 4.2(开发中)
xml
<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、ZookeeperNacos国内 → Nacos;K8s 环境 → 直接用 K8s Service
配置中心Spring Cloud Config(依赖 Git)Nacos Config、ApolloNacos(和注册中心合一)或 Apollo(权限审计更强)
服务调用OpenFeign / HTTP Interface同左,或 Dubbo RPC跨语言→HTTP;纯 Java 高频→Dubbo
网关Spring Cloud Gateway同左Gateway(基于 WebFlux,非阻塞,性能好)
熔断限流Resilience4j(Hystrix 已停维护)Sentinel国内 → Sentinel(有控制台,规则可视化配置)
分布式事务无原生方案Seata见第 6 节(先问该不该用)
链路追踪Micrometer Tracing(Sleuth 已废弃)同左 + SkyWalkingSkyWalking(无侵入探针)或 OTel

国内现实:Netflix OSS 系列(Eureka、Hystrix、Zuul、Ribbon)大部分已停止维护,国内主流是 Nacos + Sentinel + Gateway + Seata 这套组合。学习时把精力放在 Alibaba 系性价比更高,但原理是相通的——理解了服务发现的本质,换哪个注册中心都是一天上手的事。


4. 服务注册与发现:原理比 API 重要 ​

服务启动 → 向注册中心注册(IP:Port + 元数据)
              ↓
        注册中心维护服务列表
              ↓
消费者拉取列表(定时拉 + 推送变更)→ 本地缓存
              ↓
        负载均衡选一个实例 → 发起调用
              ↓
   心跳续约维持健康;心跳超时 → 实例被摘除

三个必须理解的机制:

  1. 本地缓存兜底:消费者会把服务列表缓存在本地。注册中心挂了,已有的调用不受影响——这是注册中心可以"不那么高可用"的原因。但新服务上线、扩缩容会失效。
  2. AP vs CP:Nacos 默认 AP(保可用,允许短暂的列表不一致),Zookeeper 是 CP(保一致,选主期间不可用)。服务发现场景绝大多数应该选 AP——宁可短暂读到一个已下线的实例(调用失败重试即可),也不能因为注册中心选主而让整个集群不可用。
  3. 优雅下线:实例停机前应该先从注册中心注销、等待流量摘除、再关闭进程,否则会有一段时间的请求打到已死的实例上。
yaml
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 实战 ​

java
@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 重试的危险性 ​

java
@Retryable(maxAttempts = 3, delay = 1000, multiplier = 2, jitter = 200)
public PaymentResult charge(Order order) { ... }

⚠️ 重试是把双刃剑:下游过载时,重试会让流量成倍放大,把"有点慢"变成"彻底挂"。三条铁律:

  1. 只对幂等操作重试——对非幂等的支付接口重试 3 次可能扣 3 次钱
  2. 必须用指数退避 + 随机抖动(multiplier + jitter),否则所有客户端会同时重试,形成重试风暴
  3. 重试必须和熔断配合——熔断打开时不再重试

6. ⭐ 分布式事务:先问该不该用 ​

6.1 四种方案的真实成本 ​

方案一致性性能侵入性复杂度适用
2PC / XA强一致❌ 差(全程锁资源)低中传统金融,低并发
TCC最终一致好❌ 极高(每个服务写 Try/Confirm/Cancel 三个方法)高资金类核心链路
Saga最终一致好中(要写补偿逻辑)中长流程业务(订单履约)
本地消息表 / Outbox最终一致好低低⭐ 绝大多数场景的最优解

6.2 为什么大部分场景不需要分布式事务 ​

先考虑这三个替代方案,成本低一个数量级:

① 重新审视服务边界:如果 A 和 B 总是要放在一个事务里,说明它们本来就不该拆成两个服务。这不是技术问题,是领域建模问题。合并回去,本地事务解决一切。

② 最终一致就够了:"下单成功但积分晚 3 秒到账"在业务上完全可以接受。不要把技术上的强一致执念,强加到本来能容忍延迟的业务上。

③ 本地消息表(事务性发件箱):

java
@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. 网关:流量的第一道关 ​

java
@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 让日志和链路打通(最实用的一招) ​

xml
<!-- logback-spring.xml,把 traceId 打进每一行日志 -->
<pattern>%d{HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n</pattern>
yaml
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 CloudK8s 原生 / Service Mesh
服务发现Nacos/EurekaK8s Service + DNS
配置管理Nacos/ApolloConfigMap / Secret
负载均衡客户端 LB(Spring Cloud LoadBalancer)kube-proxy / Envoy Sidecar
熔断限流Sentinel(应用内)Istio(基础设施层,业务代码零感知)
链路追踪Micrometer TracingSidecar 自动注入

趋势判断:基础设施能力(服务发现、负载均衡、重试、熔断)正在从应用层下沉到平台层。Service Mesh 的优势是业务代码零侵入、跨语言统一治理;代价是 Sidecar 的资源开销和运维复杂度。

务实结论:两者不是取代关系,而是分层关系。 2026 年的典型架构是——K8s 负责部署、发现、扩缩容;Mesh 或网关负责流量治理;Spring Cloud 的组件按需保留(比如配置中心的动态推送和灰度能力,K8s ConfigMap 目前做不到那么细)。别指望一把梭,也别全都上。


10. 本篇自测 ​

  1. 微服务的核心价值是什么?什么信号出现时才该拆?
  2. Spring Cloud 2025.0.0 和 Boot 4.0.1 为什么不兼容?选版本的正确姿势?
  3. 服务发现为什么应该选 AP 而不是 CP?
  4. 优雅停机解决了什么问题?两行配置分别是什么?
  5. 熔断器的三个状态怎么流转?熔断最重要的价值是保护谁?
  6. Sentinel 的 blockHandler 和 fallback 有什么区别?
  7. 重试为什么危险?三条铁律是什么?
  8. 分布式事务的四种方案成本对比?为什么说本地消息表是多数场景最优解?
  9. 为什么网关的自定义 Filter 里绝不能写阻塞代码?
  10. traceId 打进日志为什么是投入产出比最高的基建?

下一篇(07 · 前沿与扩展):Spring AI 怎么让 Java 后端接上大模型(含 MCP 协议)、虚拟线程 vs Goroutine 的调度模型对比、GraalVM 原生镜像的真实收益与代价、Spring Modulith 如何拯救单体、Spring Batch 处理千万级数据。2026 年该把学习时间投在哪里,这一篇给答案。

基于 Vite 强力驱动 | 纯静态轻量托管