文章目录
- SpringCloud
- SpringCloud 概述
- 集群和分布式
- 集群和分布式的区别和联系
- 微服务
- 什么是微服务?
- 分布式架构和微服务架构的区别
- 微服务的优缺点?
- 拆分微服务原则
- 什么是 SpringCloud ?
- 核心功能与组件
- 工程搭建
- 父项目的 pom 文件
- 注册中心
- RestTemplate
- 注册中心介绍
- CAP 理论
- Eureka
- 添加依赖
- 配置文件
- 启动类
- 查看
- 服务注册
- 添加依赖
- 配置文件
- 查看
- 服务发现
- 依赖、配置
- 修改代码
- Eureka 和 Zookeeper 区别
- 负载均衡
- 多次在不同的端口号上开启同一个服务
- 出现的问题
- 解决问题
- 负载均衡正式介绍
- 什么是负载均衡
- 负载均衡的一些实现
- 服务端负载均衡
- 客户端负载均衡
- SpringCloud LoadBalancer
- 负载均衡策略
- LoadBalancer 原理
- 随机选择策略
- 轮询策略
- 服务部署
SpringCloud
SpringCloud 概述
集群和分布式
集群:是将一个系统完整的部署到多个服务器上,每个服务器都能提供系统的所有服务,多个服务器通过负载均衡调度完成任务,每个服务器称为集群的节点。
分布式:是将一个系统拆分成多个子系统,多个子系统部署在多个服务器上,多个服务器上的子系统协同合作完成一个特定任务。
集群和分布式的区别和联系
- 集群是多个计算机做同样的事情,分布式是多个计算机做不同的事。
- 集群的每一个节点的功能是相同的,并且是可以替代的。分布式也是多个节点组成的系统,但是每个节点完成的任务是不同的,一个节点出现问题,这个业务就无法访问了。
- 分布式和集群在实践中,很多时候都是相互配合使用的。比如分布式的某一个节点,可能由一个集群来代替。分布式架构大多数是建立在集群上的。所以实际的分布式架构中并不会把分布式和集群单独区分,而是统称:分布式架构。
微服务
什么是微服务?
微服务是一种经过良好架构设计的分布式架构方案。
一个服务只对应一个单一的功能,只做一件事,这个服务可以单独部署运行。
分布式架构和微服务架构的区别
分布式:服务拆分,拆了就行。
微服务:指非常微小的服务,更细粒度的垂直拆分,通常指不能再拆的服务。
分布式架构侧重于压力的分散,强调的是服务的分散化,微服务侧重于能力的分散,更强调服务的专业化和精细分工。
微服务的优缺点?
优点:
缺点:
拆分微服务原则
-
单一职责原则
单一职责原则原本是面向对象程序设计中的一个基本原则,它指的是一个类应该专注于单一功能。
在微服务架构中,一个微服务也应该只负责一个功能或业务领域,每个服务应该有清晰的定义和边界,只关注自己的特定业务领域。
-
服务自治
服务自治是指每个微服务都应该具备高度自治的能力,即每个服务要能做到独立开发,独立测试, 独立构建, 独立部署,独立运行。
-
单向依赖
微服务之间需要做到单向依赖,严禁循环依赖,双向依赖。
如果一些场景确实无法避免循环依赖或者双向依赖,,可以考虑使用消息队列等其他方式来实现。
什么是 SpringCloud ?
Spring Cloud 是一套基于 Spring Boot 的微服务开发工具集,用于简化分布式系统(如微服务架构)的构建、部署和管理。它整合了多种开源组件,提供了一站式解决方案,帮助开发者快速实现服务治理、配置管理、负载均衡、熔断降级等分布式系统中的常见问题。
简单的说,Spring Cloud 就是分布式微服务架构的一站式解决方案。
核心功能与组件
- 服务注册与发现
- 组件:Eureka、Nacos、Consul
- 功能:服务自动注册到注册中心,并通过服务名实现动态发现,避免硬编码服务地址。
- 负载均衡
- 组件:Ribbon、LoadBalancer
- 功能:在多个服务实例间分配请求,支持轮询、随机等策略。
- 服务调用
- 组件:OpenFeign
- 功能:声明式的 HTTP 客户端,简化服务间的 RESTful 调用。
- 熔断与容错
- 组件:Hystrix、Resilience4j、Sentinel
- 功能:防止服务雪崩,提供降级逻辑和故障隔离。
- 配置中心
- 组件:Spring Cloud Config、Nacos
- 功能:集中管理配置文件,支持动态更新。
- API 网关
- 组件:Spring Cloud Gateway、Zuul
- 功能:统一入口,处理路由、鉴权、限流等跨服务功能。
- 分布式链路追踪
- 组件:Sleuth + Zipkin
- 功能:追踪请求在微服务间的调用路径,便于排查问题。
工程搭建
父项目的 pom 文件
指定父项目的打包方式:
添加依赖:
子项目被创建时,父项目的 pom 文件中会自动添加
上面的代码表示这里有两个子项目(模块),分别是 order-service 和 product-service 。
注册中心
RestTemplate
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它是一个同步的 REST API 客户端,提供了常见的 REST 请求方案的模版。
在项目中,当我们需要远程调用一个 HTTP 接口时,我们经常会用到 RestTemplate 这个类。这个类是 Spring 框架提供的一个工具类。
定义 RestTemplate
@Configuration
public class BeanConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
使用 RestTemplate
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate RestTemplate restTemplate;public OrderInfo selectOrderById(Integer orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);// 通过 RestTemplate 从指定 URL 获取 ProductInfo 对象String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
}
在前面的示例中 URL 是写死的。写死这个事情做的不好,如果服务器的 IP 发生变化,我们还得把所有 IP 都修改,太麻烦了,而且没有技术含量。
String url = "http://127.0.0.1:9090/product/";
注册中心介绍
有没有什么办法来解决这个问题呢?
我们可以这样做:
当服务 启动/变更 时, 向注册中⼼报道。注册中⼼记录应⽤和 IP 的关系。
调⽤⽅调⽤时,先去注册中⼼获取服务⽅的 IP,再去服务⽅进⾏调⽤。
CAP 理论
-
一致性(C):CAP 理论中的一致性,指的是强一致性。所有节点在同一时间具有相同的数据。
强一致性:主库和从库不论何时,服务器对外提供的服务都是一致的。
弱一致性:随着时间的推移,主库和从库最终达到了一致性。
-
可用性(A):保证每个请求都有响应。
-
分区容错性(P):当出现网络分区后,系统仍然能够对外提供服务。
网络分区:指分布式系统中,由于网络故障导致集群中的节点被分割成多个孤立的子集,子集之间的节点无法正常通信,但子集内部的节点仍然可以正常通信。这种现象也被称为“脑裂”(Split-Brain)。
举个例子
假设有一个分布式系统,由 5 个节点(A、B、C、D、E)组成,它们之间通过网络通信。如果由于网络故障,节点 A 和 B 之间的网络断开,那么可能会形成两个分区:
- 分区 1:节点 A、B
- 分区 2:节点 C、D、E
此时,分区 1 和分区 2 之间的节点无法通信,但分区内部的节点仍然可以正常通信。
在分布式系统中,系统间的⽹络不能100%保证健康, 服务⼜必须对外保证服务. 因此 分区容错性(P) 不可避免. 那就只能在 C 和 A 中选择⼀个. 也就是 CP 或者 AP 架构。
正常情况:

网络异常:

CP架构:为了保证分布式系统对外的数据⼀致性,于是选择不返回任何数据。
AP架构:为了保证分布式系统的可⽤性,节点2返回V0版本的数据(即使这个数据不正确)。
关于CAP的更多信息,可以看看这篇文章:一文看懂|分布式系统之CAP理论-腾讯云开发者社区-腾讯云
Eureka
添加依赖
<!-- 给客户端用 --><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency></dependencies><!-- 给服务器用 -->
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies><!-- 借助 Maven 打包 --><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
配置文件
# Eureka相关配置
# Eureka 服务
server:port: 10010
spring:application:# 这个应用的名称name: eureka-server
eureka:instance:# 主机的名称hostname: localhostclient:# 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,这里设置为falsefetch-registry: false# 表示是否将自己注册到Eureka Server,默认为true.register-with-eureka: false service-url:# 设置Eureka Server的地址,查询服务和注册服务都需要依赖这个地址defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类
@EnableEurekaServer // 开启 Eureka 的功能
@SpringBootApplication
public class EurekaServerApplication {public static void main(String[] args) {SpringApplication.run(EurekaServerApplication.class, args);}
}
查看
http://127.0.0.1:10010/
服务注册
添加依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>
配置文件
spring:application:# 配置应用名称name: product-service#Eureka Client
eureka:client:service-url:# 注册到哪里 / 从哪里拿相关信息defaultZone: http://127.0.0.1:10010/eureka/
查看
http://127.0.0.1:10010/
服务发现
依赖、配置
同服务注册。
修改代码
import org.springframework.cloud.client.discovery.DiscoveryClient;
// 注意不要引错包@Autowiredprivate DiscoveryClient discoveryClient;// 从 Eureka 中获取服务列表,括号内写应用名称List<ServiceInstance> instances = discoveryClient.getInstances("product-service");String uri = instances.get(0).getUri().toString();// 替换之前写死的 url String url = uri + "/product/" + orderInfo.getProductId();
Eureka 和 Zookeeper 区别
Eureka 和 Zookeeper 都是用于服务注册和服务发现的工具,区别如下:
- Eureka 基于 AP 原则,保证高可用。Zookeeper 基于 CP 原则,保证数据一致性。
- Eureka 每个节点都是均等的,Zookeeper 的节点区分 Leader 和 Follower 或 Observer,如果 Zookeeper 的 Leader 发生故障时,需要重新选举,选举过程集群会有短暂时间的不可用。
负载均衡
多次在不同的端口号上开启同一个服务
点击 Services 并添加应用
选择 Application
复制你要多开的服务
设置端口号,设置完毕后点击 Apply。
可以看到,已经配置好了。
出现的问题
当我们进行多次访问时,每次的 discoveryClient.getInstances(“product-service”); 拿到的列表是不固定的。
public OrderInfo selectOrderById(Integer orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);// 从 Eureka 中获取服务列表List<ServiceInstance> instances = discoveryClient.getInstances("product-service");String uri = instances.get(0).getUri().toString();// 替换之前写死的 urlString url = uri + "/product/" + orderInfo.getProductId();log.info("远程调用 url:{}", url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
显然这并不是很合理。
解决问题
假设我们想让请求平均分配到每个端口上。可以使用以下方法:
修改代码:
package com.demo.order.service;import com.demo.order.mapper.OrderMapper;
import com.demo.order.model.OrderInfo;
import com.demo.order.model.ProductInfo;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;/*** @author hanzishuai* @date 2025/03/07 19:19* @Description*/
@Slf4j
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate DiscoveryClient discoveryClient;// 计数器private AtomicInteger count = new AtomicInteger(1);// 将实例提取出来private List<ServiceInstance> instances;@PostConstructpublic void init() {// 从 Eureka 中获取服务列表instances = discoveryClient.getInstances("product-service");}// 这是之前的写法:
// public OrderInfo selectOrderById(Integer orderId) {
// OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
// String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
//
// // 从 Eureka 中获取服务列表,这里每次请求拿到的 instances 是不固定的
// List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
// String uri = instances.get(0).getUri().toString();
//
// // 替换之前写死的 url
// String url = uri + "/product/" + orderInfo.getProductId();
// log.info("远程调用 url:{}", url);
// ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
// orderInfo.setProductInfo(productInfo);
// return orderInfo;
// }public OrderInfo selectOrderById(Integer orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);// 实现平均分配int index = count.getAndIncrement() % instances.size();String uri = instances.get(index).getUri().toString();String url = uri + "/product/" + orderInfo.getProductId();log.info("远程调用 url:{}", url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
}
但是上述写法会带来一个新的问题:当实例发生变化,这里的服务并不能实时的感知到。
不要较真,在这里只是为了演示一下。
更改后的效果:
负载均衡正式介绍
什么是负载均衡
负载均衡用来在多个机器或者其他资源中,按照一定的规则合理分配负载。
比如说,有很多请求和很多服务器,负载均衡就是把这些请求合理的分配到各个服务器上。
负载均衡的一些实现
服务端负载均衡
服务端负载均衡就是在服务端进行负载均衡算法的分配。
以 Nginx 为例,请求先到达 Nginx 负载均衡器,然后通过负载均衡算法,在多个服务器之间选一个进行访问。

客户端负载均衡
服务端负载均衡就是在客户端进行负载均衡算法的分配。
以 SpringCloud 的 Ribbon 为例,请求发送到客户端,客户端从注册中心获取服务器列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问。

SpringCloud LoadBalancer
加上 @LoadBalanced 注解
@Configuration
public class BeanConfig {@Bean@LoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();}
}
修改代码
public OrderInfo selectOrderById(Integer orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);// 修改前// String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();// 修改后String url = "http://product-service/product/" + orderInfo.getProductId();log.info("远程调用 url:{}", url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
效果:
负载均衡策略
SpringCloud LoadBalancer 仅支持两种负载均衡策略:
- 轮询策略: 指服务器轮流处理用户的请求.
- 随机选择: 随机选择一个后端服务器来处理请求.
自定义负载均衡策略
public class CustomLoadBalancerConfiguration {@BeanReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),name);}
}
使用方法:
-
需要在关联负载均衡策略的配置类上添加 @LoadBalancerClient 或者 @LoadBalancerClients 注解.
// name 表示要对那个服务生效, configuration 表示你采取的负载均衡策略是什么. @LoadBalancerClient(name = "product-service",configuration = CustomLoadBalancerConfiguration.class) @Configuration public class BeanConfig {@Bean@LoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();} }
-
自定义负载均衡策略的配置类(如
CustomLoadBalancerConfiguration
)上不能使用 @Configuration 注解.原因: 如果
CustomLoadBalancerConfiguration
类被标记为@Configuration
,并且位于主应用程序组件扫描的路径下,它会被 Spring 自动加载为一个配置类。而当通过@LoadBalancerClient
的 configuration 属性引用它时,可能会导致 Spring 尝试再次加载它,从而产生冲突或重复的 bean 定义。 -
自定义负载均衡策略的配置类(如
CustomLoadBalancerConfiguration
)必须能被 Spring 容器发现。
是不是感觉与第三条第二条有矛盾?
- 在之前的回答中提到,
CustomLoadBalancerConfiguration
不能添加@Configuration
注解,这是为了避免被 Spring 自动扫描到后重复加载。- 但同时又需要确保该类能被 Spring 容器发现,这里的矛盾需要通过以下方式解决:
- 通过
@LoadBalancerClient(configuration = ...)
显式引用该类,而不是依赖组件扫描。上面的 BeanConfig 采用的就是这种方法。- 确保
CustomLoadBalancerConfiguration
不在主应用的扫描范围内,但能被@LoadBalancerClient
正确引用。
LoadBalancer 原理
tip: 按 Ctrl + alt + ← 或者 → 可以快速定位上/下次查看的位置
在 LoadBalancerInterceptor 中有一个 intercept 方法:
@Overridepublic ClientHttpResponse intercept(final HttpRequest request, final byte[] body,final ClientHttpRequestExecution execution) throws IOException {// 拿到 uri final URI originalUri = request.getURI();// 拿到 hostString serviceName = originalUri.getHost();Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));}
它会拦截所有的请求。
它做了三件事:
- 拿到 uri,也就是 http://product-service/product/1001
- 拿到 host,也就是 product-service
- 执行
接下来具体看看 this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution))
它干了什么。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {String hint = this.getHint(serviceId);LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, this.buildRequestContext(request, hint));Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);supportedLifecycleProcessors.forEach((lifecycle) -> {lifecycle.onStart(lbRequest);});// 通过 choose 方法返回了一个应用ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);if (serviceInstance == null) {supportedLifecycleProcessors.forEach((lifecycle) -> {lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));});throw new IllegalStateException("No instances available for " + serviceId);} else {return this.execute(serviceId, serviceInstance, lbRequest);}}
接下来看一下 choose 方法
public <T> ServiceInstance choose(String serviceId, Request<T> request) {// 根据应用名称获取负载均衡策略ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);if (loadBalancer == null) {return null;} else {// 如果 loadBalancer 不为空,这里又进行了一次选择Response<ServiceInstance> loadBalancerResponse = (Response)Mono.from(loadBalancer.choose(request)).block();return loadBalancerResponse == null ? null : (ServiceInstance)loadBalancerResponse.getServer();}}
如果 loadBalancer 不为空,这里又进行了一次选择接下来进入 Response<ServiceInstance> loadBalancerResponse = (Response)Mono.from(loadBalancer.choose(request)).block()
中的 choose 看看,
可以看到它有两个实现,一个是RandomLoadBalancer
,一个是RoundRobinLoadBalancer
。
随机选择策略
先来看一下 RandomLoadBalancer
public Mono<Response<ServiceInstance>> choose(Request request) {// 做了一些处理ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);// 根据请求获取到服务列表return supplier.get(request).next().map((serviceInstances) -> {// 对服务列表进行处理return this.processInstanceResponse(supplier, serviceInstances);});}
进入 processInstanceResponse
看一下
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());}return serviceInstanceResponse;}
进 getInstanceResponse
看一下
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {if (instances.isEmpty()) {if (log.isWarnEnabled()) {log.warn("No servers available for service: " + this.serviceId);}return new EmptyResponse();} else {// 生成随机数int index = ThreadLocalRandom.current().nextInt(instances.size());// 根据生成的随机数,在服务列表中选择ServiceInstance instance = (ServiceInstance)instances.get(index);// 进行下一步的处理return new DefaultResponse(instance);}}
轮询策略
进入RoundRobinLoadBalancer
public Mono<Response<ServiceInstance>> choose(Request request) {ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);return supplier.get(request).next().map((serviceInstances) -> {return this.processInstanceResponse(supplier, serviceInstances);});}
进入 processInstanceResponse
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());}return serviceInstanceResponse;}
进入 getInstanceResponse
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {if (instances.isEmpty()) {if (log.isWarnEnabled()) {log.warn("No servers available for service: " + this.serviceId);}return new EmptyResponse();} else if (instances.size() == 1) {return new DefaultResponse((ServiceInstance)instances.get(0));} else {// 计数器int pos = this.position.incrementAndGet() & 2147483647;// 通过计数器 % instances.size() 来拿到坐标ServiceInstance instance = (ServiceInstance)instances.get(pos % instances.size());return new DefaultResponse(instance);}}
服务部署
参考 博客系统笔记总结 2( Linux 相关) 中的部署 Web 项目到 Linux
本文到这里就结束啦~