SpringCloud 学习笔记1(Spring概述、工程搭建、注册中心、负载均衡、 SpringCloud LoadBalancer)
文章目录
- 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 {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用 RestTemplate
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private 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: localhost
client:
# 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,这里设置为false
fetch-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;
// 注意不要引错包
@Autowired
private 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();
// 替换之前写死的 url
String 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 {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
// 计数器
private AtomicInteger count = new AtomicInteger(1);
// 将实例提取出来
private List<ServiceInstance> instances;
@PostConstruct
public 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
@LoadBalanced
public 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 {
@Bean
ReactorLoadBalancer<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 @LoadBalanced public 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 方法:
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
// 拿到 uri
final URI originalUri = request.getURI();
// 拿到 host
String 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
本文到这里就结束啦~