1. 学习路线与整体架构认知
1.1 为什么SpringCloud值得啃下来
我最早写后端的时候,一个好几年都跟单体应用打交道。单体应用其实没那么不堪,业务量不大、团队人数少的时候,一个工程打成一个包,扔到服务器上就能跑,排查问题也简单,翻日志就行。可一旦业务膨胀,团队变成几十人,功能模块拆成好几个小组,单体的痛点就藏不住了:改一行代码要整个应用重新构建部署,一个模块内存溢出能把全站拖垮,多人同时提交代码光解决冲突就够喝一壶。
微服务的思路其实很朴素,就是把一个大应用拆成多个服务,每个服务独立开发、独立部署、独立伸缩,服务之间通过网络通信。SpringCloud做的不是让人从零造一套微服务框架,而是把注册中心、负载均衡、远程调用、熔断限流、配置管理、链路追踪这些微服务基建里必不可少的能力,统一打包成一套生态。学SpringCloud,本质上不是在学某个具体工具,而是在建立一种“分布式系统下如何组织服务”的思维方式。
这篇文章就是我个人的SpringCloud学习日志,不是官方文档搬运,也不是什么高深原理课,而是从零搭建一套最小可运行微服务集群的过程记录。适合已经会Spring Boot基本开发、但面对微服务一脸茫然的开发者读。我尽量还原每一步操作、每一次报错、每一个“为什么”的琢磨过程。
1.2 技术栈选型与版本兼容问题
关于SpringCloud的版本坑,我必须放在最前面说,因为这个坑几乎每个人都会踩。SpringCloud跟Spring Boot不同,它以“Train”命名,比如2020.0.x、2021.0.x、2022.0.x,而且每个大版本都有对应的Spring Boot版本范围。Spring Cloud Alibaba作为国内用得最多的微服务解决方案,又有自己的一套版本号。三套版本叠加在一起,稍不注意就能遇到奇怪的ClassNotFound或者Bean注入失败。
我当前落地的组合是:Spring Boot 2.7.x + Spring Cloud 2021.0.x + Spring Cloud Alibaba 2021.0.5.0 + Nacos 2.2.x。这个组合我实测下来比较稳,资料也多,遇到问题搜起来方便。如果你是第一次上手,建议直接照搬这个组合,别用太新的版本。很多初学者一上来就装最新版Spring Boot 3.x,然后发现Spring Cloud对应版本已经把很多用法改了,netty、javax到jakarta的迁移更是让人抓狂,学起来很打击信心。
选型上我没有用传统的Eureka,而是用Nacos做注册中心和配置中心,原因很直接:Eureka 2.0已经停止开发,官方后续版本也在淡出;Nacos在国内社区活跃度高,注册中心、配置中心两件事一个组件全干了,对中小团队来说少维护一套系统。服务调用上选了OpenFeign,负载均衡用Spring Cloud LoadBalancer,网关用Spring Cloud Gateway,容错用Sentinel。这套组合是目前最主流的路线,学完以后看大多数公司的微服务架构都能对上号。
2. 搭建第一个可运行的微服务集群:Nacos注册中心
2.1 最小环境准备与Nacos启动
先列一下最小环境,我用的都是很常规的配置:
- JDK 1.8 或 11
- Maven 3.6+
- IDEA 或者你习惯的IDE
- Nacos Server 2.2.x
- Docker(可选,用Docker跑Nacos最省事)
Nacos的启动有两种常见方式:一种是去官方下载压缩包解压后执行startup命令,另一种是直接用Docker跑容器。本地开发我建议用Docker,一条命令就搞定:
docker run --name nacos-server \ -e MODE=standalone \ -p 8848:8848 \ -p 9848:9848 \ -d nacos/nacos-server:v2.2.3启动完成后,浏览器访问 http://127.0.0.1:8848/nacos ,默认账号密码是nacos/nacos,能看到控制台就说明服务起来了。控制台里可以看到服务列表、配置管理、命名空间等菜单,后面我们会频繁用到它。
如果你不用Docker而是用压缩包启动,Windows下是执行 startup.cmd -m standalone,Linux/Mac下是执行 sh startup.sh -m standalone。有个小细节容易忽略:Nacos 2.x 版本除了8848端口,还会用到9848、9849这些gRPC端口,如果服务器安全组或本地防火墙只放行了8848,注册会一直报错。我第一次就在这上面卡了快一个小时。
2.2 创建父工程统一管理依赖
微服务项目通常不是一个工程,而是多个工程同时存在。最规范的做法是先建一个Maven父工程,把所有公共依赖的版本在父工程的dependencyManagement里统一管理,子服务不写版本号,直接引用即可。
父工程pom.xml核心部分我贴出来:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.7.12</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.5</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>这样做的价值在后面的学习过程中会越来越明显。你不需要关心某个starter具体是哪个版本,三套框架的版本兼容性在出厂时就已经由spring-cloud-alibaba-dependencies帮你协调好了。
2.3 服务提供者与消费者注册到Nacos
父工程建好后,我创建了两个子模块:service-provider(服务提供者)和service-consumer(服务消费者)。
service-provider的pom里需要加两个依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>然后配置application.yml:
server: port: 8081 spring: application: name: service-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848写一个简单的测试接口:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello from provider, port: 8081"; } }service-consumer也如法炮制,端口改成8082,然后写一个调用测试接口。很多人第一次会直接用RestTemplate,并配上@LoadBalanced注解,让RestTemplate具备负载均衡能力。消费者代码如下:
@SpringBootApplication public class ConsumerApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } } @RestController public class CallController { @Resource private RestTemplate restTemplate; @GetMapping("/call") public String call() { return restTemplate.getForObject("http://service-provider/hello", String.class); } }注意这里的URL用的是http://service-provider/hello,而不是http://127.0.0.1:8081/hello。这个写法是LoadBalancer在起作用:它会把service-provider这个服务名解析成Nacos里注册的具体IP和端口,然后挑一个实例发起请求。把两个服务都启动后,打开Nacos控制台,能看到两个服务已经出现在服务列表里。访问 http://127.0.0.1:8082/call ,会返回provider里的结果。到这步,一个最小可运行的微服务集群就通了。
3. 核心组件逐个击破:调用、网关、容错
3.1 OpenFeign声明式调用与超时调优
RestTemplate写出来的代码比较啰嗦,一个接口要拼URL、设置Header、处理异常,方法一多代码就跟老太太的裹脚布一样。OpenFeign提供了“声明式”的调用方式:定义一个Java接口,加上注解,Spring会自动生成这个接口的实现,你就像调用本地方法一样发起远程调用。
使用步骤很简单。启动类上加@EnableFeignClients,然后定义一个接口:
@FeignClient(name = "service-provider") public interface ProviderClient { @GetMapping("/hello") String hello(); }业务代码里直接注入这个接口调用即可:
@Service public class CallService { @Resource private ProviderClient providerClient; public String call() { return providerClient.hello(); } }OpenFeign的底层原理,说白了是动态代理。Spring启动时会扫描所有@FeignClient接口,通过JDK动态代理为每个接口生成代理对象,代理对象把这些接口方法翻译成HTTP请求,通过LoadBalancer从注册中心拿到实例地址,再发起真正的HTTP通信。这也是为什么Feign接口不能有方法体,代理类没法替接口实现里的逻辑“补全”。
实际落地的时候,OpenFeign有几个参数必须调。默认的连接超时和读取超时都很短,业务接口响应稍微慢一点就直接超时报错。我通常在配置文件里这样设置:
feign: client: config: default: connectTimeout: 5000 readTimeout: 5000如果你用的是Spring Cloud 2021及以上版本,也可以写成spring.cloud.openfeign.client.config前缀,两者效果一样。另外,如果服务间调用需要传递用户身份信息,比如token,要给Feign写一个RequestInterceptor,从当前请求上下文里取出Header再放到Feign请求里,否则用户信息会断在服务调用的链条上。这个坑在很多公司排查线上问题时都遇到过,属于必然踩的雷。
3.2 Spring Cloud Gateway路由、过滤器与跨域
服务数量变多以后,不能让每个客户端自行决定调用哪个服务。网关作为统一入口,负责路由转发、鉴权、限流、跨域处理。Spring Cloud Gateway是基于WebFlux和Netty实现的,性能和并发能力比Zuul 1.x好很多,也是目前Spring官方主推的网关方案。
建一个独立的gateway服务模块,pom里加依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>配置路由有两种方式。一种是用配置文件,这种方式对运维友好,改路由不用重新编译:
spring: cloud: gateway: routes: - id: provider-route uri: lb://service-provider predicates: - Path=/api/provider/** filters: - StripPrefix=2这里的StripPrefix=2意思是把请求路径的前两段去掉,比如客户端请求 /api/provider/hello,网关转发给provider的就是 /hello。lb://service-provider表示通过负载均衡方式从Nacos获取service-provider的实例地址。
另一种方式是用Java Bean配置RouteLocator,适合需要写复杂逻辑的场景。实际项目中,网关层常需要做统一的鉴权,我写一个全局过滤器:
@Component public class TokenGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("token"); if (token == null || token.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }getOrder()方法控制了过滤器的执行顺序,数字越小越先执行。给一个-100的目的是让它在大多数内置过滤器之前干活。需要注意的是,用Gateway的项目里不要引入spring-boot-starter-web,因为Gateway基于WebFlux的响应式模型,和传统的Servlet MVC会有冲突。当年这个坑让我在项目启动时报了莫名其妙的“Spring MVC found on classpath”错误,折腾了好久才搞清楚。
3.3 Sentinel熔断、降级与限流规则配置
微服务调用链里,任何一个下游服务崩溃都可能把上游拖死。直接让你感受一下:服务A调用服务B 100个请求阻塞等超时,服务A的线程就全被占满了,紧接着服务A自己也开始拒绝服务,这种故障的放大效应在分布式系统里非常致命。所以熔断降级不是可选项,而是必选项。
Sentinel是阿里开源的流量控制组件。使用它的时候,最核心的概念是“资源”:可以是一个方法、一个URL、一段代码。对资源定义规则后,Sentinel会在流量超过阈值时触发保护逻辑。
先加依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>写一个保护方法:
@RestController public class TestController { @GetMapping("/order") @SentinelResource(value = "order", blockHandler = "blockHandler", fallback = "fallback") public String order() { return "order success"; } public String blockHandler(BlockException e) { return "请求被限流,请稍后重试"; } public String fallback(Throwable t) { return "服务异常,走降级逻辑"; } }这里blockHandler处理的是Sentinel限流熔断触发的异常,fallback处理的是业务运行时的异常。两个方法返回的内容不同,分别对应两种情况。我遇到过不少初学者只写了blockHandler不写fallback,结果业务抛异常时用户看到的是默认错误页,看不出降级效果。
搭建Sentinel控制台有两种方式,官方提供了Dashboard的jar包,下载后直接java -jar启动,默认端口8080。配置里加上:
spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8080启动应用后,控制台就能看到所有服务。控制台里手动配置的限流规则重启应用后会丢失。生产环境要用Nacos做规则持久化,把规则配置文件推送到Nacos,Sentinel从Nacos读取规则。规则持久化以后,不用每次重启应用都去控制台重新配,算是Sentinel最关键的生产配置。
4. 配置中心与热更新
4.1 Nacos Config动态配置
服务数量一多,配置文件的管理就成了灾难。每个服务都有数据库连接、Redis连接、各种开关配置,分散在每个工程的application.yml里。改一个数据库地址得改十几个服务重新发版,谁改谁知道有多痛苦。Nacos的配置中心把配置集中管理,服务启动时从Nacos拉取配置,最重要的是支持动态刷新,改配置不用重启服务。
在已有工程里引入配置中心:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>然后在bootstrap.yml里配置Nacos config的地址:
spring: application: name: service-consumer cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml在Nacos控制台的“配置管理”里新建配置,Data ID填写service-consumer.yaml,Group默认使用DEFAULT_GROUP,配置内容就是普通的YAML格式。启动服务后,日志里会看到从Nacos配置中心拉取配置的记录。
要让配置修改后自动生效,需要配合@RefreshScope使用,或者用@NacosValue注解。@RefreshScope放在Bean上,配置刷新后Spring会重新创建一个Bean实例,自动拿到新值:
@RestController @RefreshScope public class ConfigController { @Value("${config.info:default}") private String configInfo; @GetMapping("/config") public String getConfig() { return configInfo; } }在Nacos控制台修改config.info的值并发布后,请求 /config 接口,无需重启服务就能拿到新值。这个能力在调整流量开关、功能开关、限流阈值时特别实用。
4.2 多环境管理与配置优先级
开发、测试、生产环境配置不同是很常见的需求。Nacos提供了Namespace(命名空间)和Group(分组)两个维度来隔离配置。推荐的做法是每个环境建一个Namespace,比如dev、test、prod,各自维护一套配置;相同环境内还可以用Group区分不同的业务线。
我本地的习惯是这样组织的:
- dev命名空间下放dev环境的全部配置
- test命名空间下放测试环境的配置
- prod命名空间下放生产配置
然后通过bootstrap.yml里指定namespace,服务就把配置定位到对应的环境:
spring: cloud: nacos: config: namespace: dev-namespace-id这时候优先级也需要搞清楚:本地application.yml中的配置,会被Nacos配置中心同样key的配置覆盖;多个Data ID共享时,后加载的优先级更高。也就是说,不要把敏感配置裸放在本地代码库里,而是放到对应环境的Nacos里,这样也算是一种最基础的安全兜底。
这一节再提一个配置加密问题。数据库密码、Redis密码直接写在Nacos配置里其实是明文,稍微正规一点的做法是用jasypt-spring-boot的加密机制,先对敏感项加密,配置中心里存的是密文,应用启动时解密加载。虽然还没有到对接密钥管理系统那一步,但这一层防护能挡住不少低水平的信息泄露风险。
5. 链路追踪与日志排障:定位问题不再大海捞针
5.1 一个请求经过多个服务,如何排查问题
微服务架构里,用户的一个请求可能要经过网关、服务A、服务B、服务C。如果响应变慢或者报错,传统日志方式最大痛点在于各服务日志分散,没有一个统一的标识把整个调用链串联起来。你看到service-b报了个错,却不知道这个错误是从哪个上游请求带来的。
链路追踪就是解决这个问题。核心思路是在请求进入第一个服务时生成一个全局唯一的TraceId(链路ID),这个ID随着服务间调用传递下去,每个服务内部还会生成自己的SpanId(片段ID)来表示一次调用过程。日志里都带上这两个ID,把日志集中收集后,按TraceId一查,整条调用链的相关日志全部出来了。
Sleuth是Spring Cloud早期推出的链路追踪方案,使用Spring Boot 2.7版本时,引入下面依赖即可:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> </dependency>除了把依赖加进去,还需要在配置里打开采样率和Zipkin地址:
spring: zipkin: base-url: http://127.0.0.1:9411 sleuth: sampler: probability: 1.0probability表示采样率。1.0意味着所有请求都上报,适合开发调试;生产环境建议调低到0.1甚至0.01,避免链路数据量过大影响性能。本地起一个Zipkin容器来查看链路:
docker run -d -p 9411:9411 openzipkin/zipkin把服务全部启动后,打一个请求,Zipkin界面就能看到一条完整的调用链,从网关到provider,每一步耗时都清清楚楚,哪个服务慢一眼就能看出来。
5.2 异步线程与TraceId传递的坑
链路追踪在使用中有一个经典问题:异步调用时TraceId会丢失。Java里常见的异步方式是多线程、线程池、MQ消费。Sleuth默认只在同步调用和Feign调用里传递上下文,但你在业务代码里自己new Thread异步处理时,TraceId是带不过去的。日志和主链路脱节,查问题又回到了各查各的。
解决方案常见的几种:一是对线程池进行包装,使用brave的ContextPropagator手动把TraceContext放进去;二是把异步逻辑拆分到独立服务里,通过MQ或者独立接口调用,让主请求不做异步发散;三是使用Spring的@Async调用时,确保线程池装饰器配置正确。
我个人的建议是,微服务中尽量使用MQ来解耦耗时操作,而不是业务代码里开各种Thread。纯异步线程虽然简单,但排查问题的代价在分布式环境下会大得多,这也是我把这一条单独拉出来写的原因。日志里没有TraceId的异步记录,基本等于盲人摸象。
6. 踩坑实录与学习建议
6.1 新手常见问题速查表
下面这些坑是我自己踩过,或者身边同事反复踩的,整理成一张速查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 服务注册不上,Nacos控制台看不到 | server-addr配置错误、9848端口被防火墙拦截 | 检查端口连通性,telnet试一下;用localhost或127.0.0.1都试 |
| 启动报版本冲突,类找不到 | Spring Boot/Cloud/Alibaba三套版本不匹配 | 按我前面的版本组合来,不要自己随意混搭 |
| Feign调用报404或连接拒绝 | 服务名为注册到Nacos后的小写名,或实例未正常启动 | 检查Nacos控制台服务列表;确认Feign注解里name和注册服务名一致 |
| Gateway启动失败 | 工程里同时引入了spring-boot-starter-web | 移除web相关依赖,只用WebFlux |
| Sentinel规则配置了不生效 | 资源名和代码里@SentinelResource的value不一致 | 在控制台“簇点链路”里查看实际资源名,再配置规则 |
| 配置中心改了配置不刷新 | 没加@RefreshScope或没引入actuator | 确认Bean上有@RefreshScope;确认配置项和@Value的key完全匹配 |
| 日志里没有TraceId | 链路追踪依赖没加全,或异步线程断了上下文 | ping一下Zipkin界面,看链路是否存在;异步场景手动传递上下文 |
这张表放出来以后,基本覆盖了初学者最痛苦的几个场景。具体遇到问题时,先确认版本、再确认服务是否注册、最后再查代码细节,按这个顺序排查,效率要高很多。
6.2 我的学习复盘与给新手的建议
学SpringCloud的路径如果只用一个词概括,我认为是“由简到繁”。我最初犯的错误是贪多嚼不烂:看到别人推荐什么组件都想学,结果每个组件都只停留在会配置、不知道原理的层面。后来调整策略,先用最少的组件把服务调通,再一个一个组件往里加。注册中心、负载均衡、Feign调用通,这是最小可用闭环;再加入网关、配置中心、熔断限流,逐步覆盖生产环境需要用到的能力;最后补链路追踪、日志聚合这类排障设施,整个体系才真正形成。
对于新手我特别想强调一点:不要跳过配置中心直接学网关,更不要跳过链路追踪直接学部署。很多业务系统压根没什么并发压力,Sentinel限流规则配得再花哨也用不上,反而是链路追踪在出问题时救了命。学习顺序建议按“注册中心→服务调用→网关→配置中心→链路追踪→熔断限流”的路径走,每个环节都动手起一个Demo工程,跑通了再进入下一环节。
另外,遇到报错不要第一时间就把错误扔进搜索引擎。试着先看堆栈信息,找到顶层异常的第一行,然后顺着源码的注释和官方文档判断原因。SpringCloud的报错信息大部分已经把问题描述得很清楚了,自己独立思考一次,比直接复制粘贴别人的答案印象深得多。
这篇学习日志我会继续补充下去。下一步打算把分布式事务、接口幂等、容器化部署这几个大件啃完,再回来更新。最后分享一个个人体会:别指望收藏夹里吃灰的资料能帮你学会微服务,真正让能力沉淀下来的是每次踩坑后的复盘,以及亲手把一个完整链路跑通带来的成就感。照着这篇先把最小集群搭起来,然后换成你自己的业务场景去折腾,进步会比你想象得快。