什么是分布式系统?

架构分:单体和分布式。集群只是一种物理形态,分布式是工作方式

架构演进 定义 优点 缺点
单体架构 所有功能模块都在一个项目里 开发部署简单 无法应对高并发
集群架构 单体的多服务器版本 解决大并发 问题1:模块升级麻烦
问题2:多语言团队交互不通
分布式架构 一个大型应用被拆分成很多小应用分布部署在各个机器 解决了单体+集群的问题

版本关系

Spring Boot 版本 Spring Cloud 版本 Spring Cloud Alibaba 版本 主要特点与说明
3.5.x 2025.0.x 2023.0.3.3 当前较新的稳定组合:需要JDK 17或更高版本,采用Jakarta EE命名空间。
3.2.x 2023.0.x 2022.0.x 需要JDK 17+,采用Jakarta EE。
2.7.x 2021.0.x 2021.0.6.2 支持JDK 8的最终LTS组合:生态成熟稳定,是企业级项目的稳妥选择。

环境搭建

使用 JDK17
根目录pom文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>services</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.study</groupId>
<artifactId>cloud_demo</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>

建一个 service 的 module,在这个 module 下面建 service-order 和 service product 模块。完成基本环境构建。

注册与发现

注册与发现有很多组件,如Nacos、Eureka、Consul、Zookeeper、Dubbo等,我们以阿里巴巴的nacos为例。
nacos 官网|快速开始,这里要下载nacos的服务端:nacos server 账号密码都是nacos
在nacos的bin目录下启动cmd窗口,输入启动命令:startup.cmd -m standalone,访问 http://localhost:8848/nacos 即可访问nacos服务。

引入依赖

在service的根pom.xml 引入 nacos 的依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

在 service-order, service-product都加依赖

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

服务注册

service-order 模块中的 application.properties 添加如下配置:

1
2
3
server.port=8000
spring.application.name=service-order
spring.cloud.nacos.server-addr=127.0.0.1:8848

然后创建 Springboot 启动类。

service-product 也一样,端口号和名称变一下就好。

启动服务后去nacos上,查看 服务管理/服务列表 就可以发现服务注册上来了,其次可以启动多个服务副本(勾选 Allow multiple instances,勾选 Program arguments,填入–server.port=9001),会发现都会注册上 nacos

服务发现

product 的启动类加上 @EnableDiscoveryClient
去测试包(和启动类包路径一致)测试下面的服务发现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootTest
public class ProductApplicationTest {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
NacosDiscoveryClient nacosDiscoveryClient;//二者效果一样,这个依赖nacos

@Test
public void discoveryClientTest(){
List<String> services = discoveryClient.getServices();
for (String service : services) {
System.out.println("service = " + service);
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println("instance.getHost() = " + instance.getHost());
System.out.println("instance.getPort() = " + instance.getPort());
}
}
}

@Test
public void nacosDiscoveryClientTest(){
List<String> services = nacosDiscoveryClient.getServices();
for (String service : services) {
System.out.println("service = " + service);
List<ServiceInstance> instances = nacosDiscoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println("instance.getHost() = " + instance.getHost());
System.out.println("instance.getPort() = " + instance.getPort());
}
}
}
}

远程调用

order 会查询 product 的信息去生成订单,这之间就涉及到了一个远程调用。
因此,新建一个 model 模块,和services模块平级,实体类的统一管理。
在 model 的pom文件中引入 lombok 的依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
</dependencies>

创建 bean 信息
Order:com.study.bean.Order.java

1
2
3
4
5
6
7
8
9
@Data
public class Order {
private Long id;
private BigDecimal totalAmount;
private Long userId;
private String nickName;
private String address;
private List<Product> productList;
}

Product:com.study.bean.Product.java

1
2
3
4
5
6
7
@Data
public class Product {
private Long id;
private BigDecimal price;
private String productName;
private int num;
}

在services的pom文件中导入model,就可以用了

1
2
3
4
5
<dependency>
<groupId>com.study</groupId>
<artifactId>model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

service-prodcut

在 service-product 中创建 controller

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class ProductController {
@Autowired
ProductService productService;

@GetMapping(value = "/product/{id}")
public Product getProductById(@PathVariable("id") Long productId) {
System.out.println("正在远程调用service-product...");
Product product = productService.getProductById(productId);
return product;
}
}

创建 service 接口

1
2
3
public interface ProductService {
Product getProductById(Long productId);
}

创建实现 service 接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class ProductServiceImpl implements ProductService {
@Override
public Product getProductById(Long productId) {
Product product = new Product();
product.setId(productId);
product.setPrice(new BigDecimal("99"));
product.setProductName("苹果-" + productId);
product.setNum(11);
return product;
}
}

重启测试:http://localhost:9000/product/2

1
2
3
4
5
6
{
"id": 2,
"price": 99,
"productName": "苹果-2",
"num": 11
}

service-order

同样的,在 service-order 中创建 controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class OrderController {
@Autowired
OrderService orderService;

@GetMapping(value = "/create")
public Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {
Order order = orderService.createOrder(userId, productId);
return order;
}
}

创建 service 接口

1
2
3
public interface OrderService {
Order createOrder(Long userId, Long productId);
}

创建实现 service 接口的实现类。
此处需要对service-product服务进行远程调用,稍后处理,先测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class OrderServiceImpl implements OrderService {
@Override
public Order createOrder(Long userId, Long productId) {
Order order = new Order();
order.setId(1L);
//TODO 总金额
order.setTotalAmount(new BigDecimal("0"));
order.setUserId(userId);
order.setNickName("张三");
order.setAddress("火星");
//TODO 远程查询商品列表
order.setProductList(null);
return order;
}
}

重启测试:http://localhost:8000/create?userId=2&productId=23

1
2
3
4
5
6
7
8
{
"id": 1,
"totalAmount": 0,
"userId": 2,
"nickName": "张三",
"address": "火星",
"productList": null
}

完善远程调用

service-order 创建包 config,将RestTemplate加入到spring容器

1
2
3
4
5
6
7
@Configuration
public class OrderConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

结合之前的服务发现,完善Order的远程调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
RestTemplate restTemplate;


@Override
public Order createOrder(Long userId, Long productId) {
Product product = getProductFromRemote(productId);

Order order = new Order();
order.setId(1L);
// 总金额=价格*数量
BigDecimal price = product.getPrice();//价格
int num = product.getNum();//数量
order.setTotalAmount(price.multiply(new BigDecimal(num)));//总价
order.setUserId(userId);
order.setNickName("张三");
order.setAddress("火星");
// 远程查询商品列表
order.setProductList(Arrays.asList(product));
return order;
}

//远程调用获取商品信息
public Product getProductFromRemote(Long productId) {
//1、获取到商品服务所在的所有机器IP+port
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
ServiceInstance instance = instances.get(0);
//远程URL
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + productId;
log.info("远程请求:{}", url);
//2、给远程发送请求
Product product = restTemplate.getForObject(url, Product.class);
return product;
}
}

负载均衡

使用loadBalancerClient

在services模块加入依赖

1
2
3
4
5
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

然后测试

前提:只能负载均衡注册到nacos的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
public class OrderApplicationTest {
@Autowired
LoadBalancerClient loadBalancerClient;

@Test
public void test() {
ServiceInstance choose = loadBalancerClient.choose("service-product");
System.out.println("choose.getHost()+choose.getPort() = " + choose.getHost() + ":" + choose.getPort());
choose = loadBalancerClient.choose("service-product");
System.out.println("choose.getHost()+choose.getPort() = " + choose.getHost() + ":" + choose.getPort());
choose = loadBalancerClient.choose("service-product");
System.out.println("choose.getHost()+choose.getPort() = " + choose.getHost() + ":" + choose.getPort());
choose = loadBalancerClient.choose("service-product");
System.out.println("choose.getHost()+choose.getPort() = " + choose.getHost() + ":" + choose.getPort());
choose = loadBalancerClient.choose("service-product");
System.out.println("choose.getHost()+choose.getPort() = " + choose.getHost() + ":" + choose.getPort());
}
}

输出:

1
2
3
4
5
choose.getHost()+choose.getPort() = 192.168.1.4:9001
choose.getHost()+choose.getPort() = 192.168.1.4:9000
choose.getHost()+choose.getPort() = 192.168.1.4:9002
choose.getHost()+choose.getPort() = 192.168.1.4:9001
choose.getHost()+choose.getPort() = 192.168.1.4:9000

有看出,采用的默认是轮询

将远程调用商品的方法getProductFromRemote() 替换为下面这个 getProductFromRemoteWithLoadBalance()

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
LoadBalancerClient loadBalancerClient;
//阶段二:加入负载均衡
public Product getProductFromRemoteWithLoadBalance(Long productId) {
//1、获取到商品服务所在的所有机器IP+port
ServiceInstance choose = loadBalancerClient.choose("service-product");
//远程URL
String url = "http://" + choose.getHost() + ":" + choose.getPort() + "/product/" + productId;
log.info("远程请求:{}", url);
//2、给远程发送请求
Product product = restTemplate.getForObject(url, Product.class);
return product;
}

重启请求6次:http://localhost:8000/create?userId=2&productId=23

输出

1
2
3
4
5
6
2025-11-04T13:28:06.454+08:00  INFO 7716 --- [service-order] [nio-8000-exec-1] c.study.service.Impl.OrderServiceImpl    : 远程请求:http://192.168.1.4:9000/product/23
2025-11-04T13:28:08.260+08:00 INFO 7716 --- [service-order] [nio-8000-exec-2] c.study.service.Impl.OrderServiceImpl : 远程请求:http://192.168.1.4:9002/product/23
2025-11-04T13:28:09.371+08:00 INFO 7716 --- [service-order] [nio-8000-exec-3] c.study.service.Impl.OrderServiceImpl : 远程请求:http://192.168.1.4:9001/product/23
2025-11-04T13:28:10.411+08:00 INFO 7716 --- [service-order] [nio-8000-exec-4] c.study.service.Impl.OrderServiceImpl : 远程请求:http://192.168.1.4:9000/product/23
2025-11-04T13:28:11.473+08:00 INFO 7716 --- [service-order] [nio-8000-exec-7] c.study.service.Impl.OrderServiceImpl : 远程请求:http://192.168.1.4:9002/product/23
2025-11-04T13:28:12.331+08:00 INFO 7716 --- [service-order] [nio-8000-exec-6] c.study.service.Impl.OrderServiceImpl : 远程请求:http://192.168.1.4:9001/product/23

使用@LoadBalanced注解

在之前的 OrderConfig 下的 restTemplate() 方法上面加上 @LoadBalanced

将远程调用商品的方法getProductFromRemoteWithLoadBalance() 替换为下面这个 getProductFromRemoteWithLoadBalanceAnnotation()

1
2
3
4
5
6
7
8
//阶段三:于注解的负载均衡
public Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId) {
//给远程发送请求;;service-product会被动态替换
String url = "http://service-product/product/" + productId;
Product product = restTemplate.getForObject(url, Product.class);
return product;
}

思考:注册中心宕机,远程调用还能成功吗?

  1. 远程中心没有启动前宕机了,不能调用成功。
  2. 远程中心启动后宕机了,可以调用成功,因为存在缓存。

配置中心

也是使用的nacos,但是要在 services 引入配置的依赖

1
2
3
4
5
<!--配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

导入了配置中心的依赖但是没设置(用到)配置中心就会报错(service-product)

1
2
3
Add a spring.config.import=nacos: property to your configuration.
If configuration is not required add spring.config.import=optional:nacos: instead.
To disable this check, set spring.cloud.nacos.config.import-check.enabled=false.

解决办法,关闭自动检查

1
2
#暂未用到配置中心功能,需要关闭配置检查
spring.cloud.nacos.config.import-check.enabled=false

创建配置

在nacos中的 配置管理/配置列表 中新建配置:service-order.properties,选择格式 PROPERTIES

1
2
order.timeout=10min
order.auto-confirm=20d

点击发布。

在 service-order 中指定配置:

1
spring.config.import=nacos:service-order.properties

然后再 order的controller中,可以获取到配置:

1
2
3
4
5
6
7
8
9
@Value("${order.timeout}")
String orderTimeout;
@Value("${order.auto-confirm}")
String orderAutoConfirm;

@GetMapping("/config")
public String getConfig() {
return "OrderTimeout+OrderAutoConfirm = " + orderTimeout+" : " + orderAutoConfirm;
}

重启程序,访问:http://localhost:8000/config

自动刷新

像刚刚的设置,会产生一个问题:nacos配置中修改后,不重启服务,发请求不能自动更新修改后的数据。
这里有三个自动刷新的办法去解决:

@RefreshScope

实时更新配置显示的办法:在@RestController上加@RefreshScope即可

参考