干 Java 开发的,基本绕不开 Spring Cloud。我最早接触微服务的时候,被注册中心、网关、配置中心、负载均衡、熔断降级这些概念砸得头晕。网上资料多,但东一篇西一篇,真正想从零到一搭一套能跑的 Spring Cloud 微服务系统,才发现选型、版本、配置每一样都有讲究,稍不注意就是一堆启动报错。
这篇就把我实际搭建 Spring Cloud 微服务系统的过程写出来:核心组件为什么这样选、项目结构怎么分、依赖怎么配、启动顺序是什么,以及开发中经常踩的坑。适合准备做微服务改造的 Java 开发,也适合面试前想把整个体系重新理一遍的朋友。文章不会绕概念,直接给可操作的方案。
1. 先想清楚:到底要不要上微服务
1.1 单体应用被吐槽的“三宗罪”
先说单体应用的问题。业务刚起步、团队就几个人、每天几十万请求的时候,单体其实是最舒服的,一个 Spring Boot 包丢服务器上就能跑。真正让人想拆微服务的,通常来自三个压力。
第一个是并发压力。某些核心接口流量特别高,但你没法只给其中一个模块扩容,只能把整个应用复制好几份,非核心模块把资源全吃掉,扩了等于没扩。第二个是协作压力。几十个人在一个代码仓库里改同一个工程,合并冲突、发布互相影响,效率肉眼可见往下掉。第三个是故障隔离差。一个接口把内存打满,整个服务全部不可用,其他无关功能跟着陪葬。
所以,微服务的本质不是把代码拆小,而是把“能力边界”和“故障边界”一起拆开。每个服务可以独立开发、独立部署、独立扩容,这是它最大的价值,但代价也很真实:网络调用取代本地方法调用、分布式事务、服务发现、配置管理、链路追踪……这些原本不用管的问题全冒出来了。网上搜到的微服务架构图基本都差不多,客户端走网关,网关再分发到一堆服务,底下挂着注册中心、配置中心、消息队列、缓存。图画起来简单,落地布线才是真功夫。
1.2 Dubbo 和 Spring Cloud 到底怎么选
这个选择题几乎每个团队都面临过。我的建议很简单:如果技术栈以 Java 为主,而且已经深度使用 Spring Boot,直接走 Spring Cloud 通常更顺。
Dubbo 的优势在 RPC 性能和服务治理,默认走 TCP 长连接,接口性能要求特别高、服务治理精细到方法级别的场景里它更强。但 Dubbo 本身更偏重服务通信,网关、配置、链路追踪这些配套组件没有完整统一方案,需要自己组合。Spring Cloud 走 HTTP REST,性能上多一层序列化开销,但它属于全家桶式生态:网关、注册中心、配置中心、熔断限流、分布式链路都有对应组件,不需要自己拼拼凑凑。
这里给一张我面试时经常拿来对比的表:
| 对比维度 | Dubbo | Spring Cloud |
|---|---|---|
| 通信协议 | 默认 RPC 协议,TCP 长连接 | HTTP REST,基于 Feign/WebClient |
| 服务注册发现 | 支持 Zookeeper、Nacos 等 | Nacos、Eureka、Consul 等 |
| 生态完整度 | 偏重 RPC 与服务治理 | 从网关到链路追踪全家桶 |
| 负载均衡 | 内置多种策略 | LoadBalancer + 定制 |
| 跨语言支持 | 有泛化调用等机制 | HTTP 方式天然利于多语言接入 |
| 学习成本 | 相对低 | 组件多,需要时间理解 |
不过现在不少项目会用“Spring Cloud Alibaba + Dubbo”的混合方案:注册和配置走 Nacos,服务内部通信走 Dubbo,对外提供 HTTP 用 Spring Cloud。新项目我还是建议先选一条主路径,等团队真跑顺了再考虑混合,不然排查问题的时候多一层都多一种麻烦。
1.3 版本选型:先把搭配关系定死
版本坑是新手最容易踩的。Spring Cloud 和 Spring Boot、Spring Cloud Alibaba 三者版本必须匹配,不能用“最新版”的冲动直接往上堆。我实际用下来比较稳的搭配有两组:
- 经典稳定组合(JDK 8 或 11):Spring Boot 2.7.18 + Spring Cloud 2021.0.5 + Spring Cloud Alibaba 2021.0.5.0 + Nacos Server 2.2.3
- 新项目组合(JDK 17):Spring Boot 3.2.x + Spring Cloud 2023.0.x + Spring Cloud Alibaba 2023.0.3.x + Nacos Server 2.3.x
建议新项目直接用第二组,Spring Boot 3 和 Spring Cloud 2023 这套已经稳定很长时间了。JDK 8 就别硬拉 Spring Boot 3.x,JDK 17 也不要配太旧的 Spring Cloud Alibaba,否则很容易碰到 Lombok、CGLIB、反射访问等兼容问题。
版本不对最常见的表现是:依赖下载失败、启动直接 NoSuchMethodError、客户端连不上 Nacos。看起来像环境问题,其实根源是版本矩阵没对齐。所以动手写代码之前,去 Spring Cloud Alibaba 官方网站翻一下版本说明,把关系定死,能省掉两三天排查时间。
2. Spring Cloud 核心组件逐个拆
2.1 Nacos:注册中心和配置中心一次搞定
Nacos 是我现在最常用的注册中心,它也兼任配置中心。以前用 Eureka 只能做服务注册发现,做动态配置要再上一套 Config Server 加 Bus,运维得维护两套系统。Nacos 把这两个场景合在一起,控制台里能看到所有服务实例的上下线和健康状态,还能动态改配置并自动推给客户端,用下来确实省事。
核心工作机制一句话:Provider 启动时把自己的 IP、端口、服务名注册到 Nacos,并定期心跳续约;Consumer 从 Nacos 拿到服务列表,选择其中一个实例发起调用。配置中心则是把动态配置项从 application.yml 里挪到 Nacos 配置管理中,修改配置后客户端可以感知变化并刷新生效。
注册中心和配置中心是微服务的基石,生产环境必须部署集群,至少三个节点。单机 Nacos 平时开发调试没问题,一旦它挂了,整个集群的服务发现和配置变更都会停摆,线上事故往往就是从这种“图省事”开始的。
2.2 Gateway:所有流量从这里进
Spring Cloud Gateway 是当前微服务网关的标配,基于 WebFlux 和 Reactor 实现,性能不错,支持路由转发、断言、过滤器、限流、跨域等功能。它的作用相当于整个系统的门卫:客户端不直接调后端服务,而是统一打到网关,由网关根据规则转发到对应的 service。
网关配置的三个核心概念是 Route、Predicate、Filter。Route 定义路由条目,Predicate 决定什么请求走这条路由,Filter 负责转发前后做处理。举个例子,Path=/api/user/**表示请求路径以 /api/user/ 开头时命中路由,配合StripPrefix=1过滤器可以把前缀去掉,让 /api/user/1 转成 user-service 的 /user/1。
网关层不写业务逻辑,所以代码量很少,绝大多数工作都在配置文件里。我在项目中通常把全局跨域、统一鉴权、访问日志、灰度发布这些放在网关层处理,避免每个微服务各自重复实现。
2.3 OpenFeign 与负载均衡:服务间调用不再硬编码 IP
微服务之间互相调用,最难受的写法是硬编码另一个服务的 IP 和端口。对方一扩容、一迁移,代码就要跟着改。OpenFeign 是 Spring Cloud 里的声明式 HTTP 客户端,你只需要定义一个接口,加上@FeignClient(name = "order-service"),Spring 会通过动态代理生成实现类,调用时自动完成服务发现和负载均衡。
要理解 OpenFeign,先理解 Java 动态代理:接口没有实现类,运行时由代理对象拦截方法调用,拼装 HTTP 请求、选择目标实例、发起调用、把结果转换回方法返回值。整个过程对调用方透明,看起来就像本地方法调用。
Spring Cloud 2020 之后 Ribbon 退役,默认负载均衡组件是 Spring Cloud LoadBalancer,默认是轮询策略,需要自定义策略时可以自己实现负载均衡接口。OpenFeign 还支持超时配置、请求重试、日志增强,但要注意:开了重试就要保证接口幂等,否则重复扣款、重复下单这种场景会出大问题。
2.4 Sentinel:限流熔断,救急保命
微服务链路一长,任何一个服务变慢,都可能通过同步调用把下游拖垮。Sentinel 负责限流和熔断降级,是阿里开源组件,和 Hystrix 相比更轻量、规则配置更灵活,限流甚至能精确到方法和参数。
常用场景是三类。第一,普通接口限流:QPS 设为 200,超过就快速失败或者排队等待。第二,熔断降级:下游调用成功率低于 80% 时开启熔断,走 fallback 逻辑,而不是无限等待直到线程池打满。第三,热点参数限流:同一个接口针对不同参数值设置不同阈值,比如普通用户限流 100,VIP 用户限流 1000。
在微服务搭建早期,Sentinel 不一定马上要接入,但线上出过几次事故之后你会想立刻补上。我的建议是骨架阶段就把 Sentinel 依赖和控制台接好,规则可以慢慢补充,后面要加限流就不用改代码。
2.5 链路追踪:微服务必须有的“监控眼”
请求在微服务之间跳转,出问题后你不知道它经过了哪些服务、每段耗时多少,这是微服务排障最痛苦的点之一。链路追踪需要做三件事:生成全局唯一 traceId、在服务间传递、把所有 span 串联展示。Spring Cloud Sleuth 在老版本项目里很常见,新项目可以用 Micrometer Tracing 搭配 Zipkin,也可以直接用 SkyWalking 做无侵入 Agent 方案。
个人经验:链路追踪在项目规模小的时候看着没用,等一次调用要跨四五个服务、一个接口慢了 3 秒却找不到瓶颈的时候,你就知道它的价值了。搭建最小骨架时,可以先在日志里把 traceId 打印出来,后面逐步接入完整的链路分析系统。
3. 实战:从零搭一套最小可运行微服务
3.1 环境准备:JDK、Maven、Nacos
先把环境弄到能跑。JDK 新项目建议直接用 17,Windows 配置 JAVA_HOME,Path 里加%JAVA_HOME%\bin,然后执行java -version验证。Maven 记得配好本地仓库路径和国内镜像源,不然下载依赖能让人崩溃。
Nacos 下载解压后,Linux 和 Mac 执行sh startup.sh -m standalone,Windows 执行startup.cmd -m standalone,默认控制台地址是http://localhost:8848/nacos,初始账号密码都是 nacos。如果 8848 端口被占用,在 conf/application.properties 里改 server.port,但客户端所有配置也要跟着改。
注意:单机启动 Nacos 一定要带
-m standalone参数,不带的话会以集群模式启动,然后报各种连接失败。这个坑我见过不下五次。
3.2 父工程与公共模块
创建一个 Maven 父工程,负责统一依赖版本管理。我用 spring-boot-starter-parent 作为父 POM,同时把 spring-cloud-dependencies 和 spring-cloud-alibaba-dependencies 的 BOM 引入 dependencyManagement,子模块不需要再写版本号。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.x</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2023.0.3.x</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>公共模块可以放统一返回体 Result、全局异常处理、常量类、通用工具类。这里要提醒一句:公共模块尽量少放业务相关的类,不然各个服务都依赖它,最后很容易把微服务做成“分布式单体”——代码拆了,依赖还是一团。
如果你赶时间,也可以直接拿若依微服务版本这类开源脚手架改,它把这些组件都配好了。但我的建议是,哪怕用脚手架,也要先理解注册中心、网关、Feign 各自干什么,不然出了 bug 都不知道去哪查。
3.3 user-service:让服务注册进 Nacos
新建一个 Spring Boot 模块 user-service,依赖加 spring-boot-starter-web 和 spring-cloud-starter-alibaba-nacos-discovery。入口类上加@SpringBootApplication就行,新版会自动开启服务发现,不需要手动加@EnableDiscoveryClient,加上也不影响。
配置文件核心内容:
server: port: 8081 spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848写一个简单的查询接口:
@RestController @RequestMapping("/user") public class UserController { @GetMapping("/{id}") public String getUser(@PathVariable Long id) { return String.format("user-%s-%d", id, System.currentTimeMillis()); } }启动后去 Nacos 控制台的服务列表里看,user-service 已经注册上来了,状态是健康。服务注册成功的关键就是 spring.application.name 和 server-addr 都不能配错。
3.4 order-service:用 Feign 调用别人的服务
order-service 除了 web 和 nacos-discovery,还需要额外加 spring-cloud-starter-openfeign。入口类上添加@EnableFeignClients,然后定义一个 Feign 接口:
@FeignClient(name = "user-service") public interface UserFeignClient { @GetMapping("/user/{id}") String getUser(@PathVariable("id") Long id); }这里重点提醒:@FeignClient里的 name 必须和目标服务在 Nacos 注册的 spring.application.name 完全一致,大小写也得对,否则调用时找不到服务。接口路径和参数要和目标接口完全匹配,不然会 404。
接着在 OrderController 里注入使用:
@RestController @RequestMapping("/order") public class OrderController { private final UserFeignClient userFeignClient; public OrderController(UserFeignClient userFeignClient) { this.userFeignClient = userFeignClient; } @GetMapping("/{id}") public String createOrder(@PathVariable Long id) { return "order created, user=" + userFeignClient.getUser(id); } }启动后访问http://localhost:8082/order/1,会发现它先调了 user-service,再把结果拼在响应里返回。这就是最基本的服务间调用链路。
3.5 Gateway:统一入口与路由转发
新建 gateway 模块,只需要 spring-cloud-starter-gateway 和 nacos-discovery,千万不要引入 spring-boot-starter-web。Gateway 基于 WebFlux,和 MVC 的 starter-web 冲突,启动直接报错,这个坑踩过一次就不会忘。
配置文件:
server: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1这里的lb://user-service表示从 Nacos 拿到 user-service 的实例做负载均衡,StripPrefix=1表示把/api/user/1转成/user/1。测试时直接访问http://localhost:8080/api/order/1,能拿到 order-service 的返回,并且调用链路是网关到 order-service,再由 order-service 通过 Feign 到 user-service。
3.6 启动顺序与完整验证
实际启动顺序建议:Nacos -> user-service -> order-service -> Gateway。服务提供者不启动,消费者启动时会解析不到服务实例,虽然大多数情况不影响启动,但按顺序来能少看一些警告。
启动完成后做三件事验证:
- Nacos 控制台服务列表能看到 user-service、order-service、gateway-server 三个实例。
- 直接访问 order-service 的接口,确认 Feign 调用成功。
- 通过网关访问同一个接口,确认路由转发正常。
如果这三点都通过,一套最小可运行的 Spring Cloud 微服务链路就算通了。后续加 Sentinel 限流、配置中心、链路追踪,都是在这个骨架上加东西,骨架稳了,后面才敢往上堆。
4. 常见问题与避坑实录
4.1 Java 环境变量或内存不足导致起不来
新机器上最容易报“java 不是内部或外部命令”,十有八九是 JAVA_HOME 或 Path 没配好。设置好系统变量后,最好新开一个终端窗口再验证,缓存的环境变量不会自动刷新。
Maven 编译报OutOfMemoryError: insufficient memory也很常见,一般是 MAVEN_OPTS 给的内存太小。可以在环境变量里设置:
MAVEN_OPTS=-Xms512m -Xmx1024mIDEA 编译报内存不足,就去 Help -> Change Memory Settings 里调大堆内存。Nacos 启动 OOM 则修改 bin 目录下 startup 脚本的 JVM 参数。这类问题不复杂,主要靠日志提示定位,别一上来就怀疑代码。
4.2 Lombok 和 JDK 版本打架
IDE 里突然报java: You aren't using a compiler supported by lombok, so lombok will not work with your project,常见原因是 JDK 版本太新、Lombok 版本太旧。JDK 17 配合 Lombok 1.18.20 之前的版本,大概率会出兼容问题,升级 Lombok 到 1.18.30 以上基本能解决。
如果你用 IDEA,还要检查两处:Settings -> Build -> Compiler -> Annotation Processors 是否勾选了 Enable annotation processing;Lombok 插件是否正常安装。Maven 命令行编译没问题但 IDEA 编译报错,基本就是 Annotation Processing 没开。
4.3 RedisTemplate 自增报 not integer or out of range
redisTemplate.opsForValue().increment(key)报ERR value is not an integer or out of range,说明这个 key 在 Redis 里存的值不是整数类型。我排查过好几次,多数是之前手动 set 了个字符串,或者某个地方用 JSON 序列化写入了值,类型对不上。
先用 Redis 命令确认:type key查看类型,get key查看内容。如果是脏数据,删除后让代码重新写入;如果要存计数,统一用 StringRedisTemplate 或者确保 value 类型是 Long/Integer。Redis 里存的是浮点数时,increment 同样会失败,需要重新设计存储结构。
4.4 NoClassDefFoundError: java/applet/Applet
老项目升级 JDK 时会偶发Uncaught Exception java.lang.NoClassDefFoundError: java/applet/Applet in thread...。JDK 9 之前 Applet API 还在,JDK 11 之后 java.applet 包已经不可用,但某些旧依赖或反射代码仍然尝试加载它。
排查方法:启动时加-verbose:class参数,看哪个类触发 Applet 加载,找到后升级对应依赖,或者把相关反射代码删掉。不建议靠--add-modules硬开,包本身在 JDK 11 中已经移除,加了也白加。老服务实在要跑,就用 JDK 8 作为过渡,新建服务别再用这类旧依赖。
4.5 Spring Cloud Gateway 能做集群吗?
能,而且很容易。Gateway 本身是无状态组件,路由规则从配置中心读取,集群部署只是多启几个实例,前面加 Nginx 或云负载均衡器做流量分发即可。
但是要注意三点。第一,如果用了基于本地内存的限流和缓存,集群实例之间不共享,需要改为 Redis 等外部存储。第二,WebSocket 长连接场景,负载均衡器最好配置会话保持,否则连接会断。第三,路由配置变更依赖配置中心动态推送,别一台台手动改,容易漏。
4.6 Nacos 挂了,服务真的会瘫痪吗?
在已有连接不失效的情况下,短时间内很多服务还能正常通信,因为消费者本地缓存了服务列表,提供者本地的注册信息也还在。但问题在于:某个实例宕机后,消费者无法从 Nacos 感知变化,会把请求继续打给死掉的实例,此时只能依靠客户端重试和熔断降级兜底,新服务上线、配置变更这些操作也都会受影响。
生产环境 Nacos 至少部署三个节点组成集群,通过 Raft 等算法自动选主,这样才能保证注册中心自身的高可用。别拿单节点 Nacos 当生产环境用,这个风险不值得赌。
5. 实操之后的几点体会
5.1 先把一条调用链路跑通
我见过很多同学一上来就想把注册中心、网关、配置中心、Sentinel、链路追踪、分布式事务全部配齐,最后被一堆组件的版本和配置折磨到放弃。正确的做法是先搭最简骨架:注册中心加两个服务,一个用 Feign 调另一个,再用网关统一入口。这条链路通了,微服务的核心概念基本就落地了,后面再逐个加组件。一次只引入一个变化,出问题了也好定位。
而且这条最简链路对我理解面试里那些概念帮助特别大。以前看八股文,网关、熔断、服务发现都是抽象名词,真正动手跑一遍之后,这些词就变成了具体的配置和报错日志,印象完全不一样。
5.2 后续扩展方向
骨架跑通之后,下一步建议依次做这几件事:
先把 Nacos 配置中心接上,把动态配置项挪过去,学会用命名空间区分环境,省掉重启发布。然后接 Sentinel,给核心接口配置限流规则,至少保证流控能力可用。接着接链路追踪,至少能在日志里看到 traceId,再考虑完整的调用链可视化。
文件上传、对象存储这类能力,如果不想一开始就绑定云厂商,可以先接 MinIO 这类自托管方案,后续再平滑迁移。最后就是 CI/CD:每个服务独立构建镜像、独立流水线部署,这一步做到之后,微服务“独立发布”的价值才算真正体现出来。
我在实际项目里最大的教训是:微服务只是把单体时代的复杂度换了一种形态存在,服务拆分之后,分布式部署、配置管理、服务治理都是真实的工作量。如果团队连 Spring Boot 单体都维护得很吃力,先别急着拆服务,把一个模块的链路跑通、把监控和容灾补齐,再逐步往外扩。技术选型永远要为团队和业务服务,而不是为了在架构图上多画几个框。