1. 父子工程的搭建
首先创建一个 Maven 项目,删除 src ,只保留 pom.xml
然后来进行 pom.xml 的相关配置
<?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>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>order-service</module>
<module>product-service</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<mybatis.version>3.0.3</mybatis.version>
<mysql.version>8.0.33</mysql.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>${mybatis.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在上面的配置中,使用 properties 来进行版本号的统一管理,使用 dependencyManagement 来管理依赖,并通过<packaging>pom</packaging>
来生命父工程的打包方式为 pom
dependencyManagement 和 dependencies
- dependencies:将依赖的
jar
包添加到父项目的dependencies
部分时,这些依赖会被父项目及其子项目继承,子项目无需额外操作即可自动包含这些依赖。 - dependencyManagement: 仅用于声明依赖,不会将
jar
包引入到父项目或子项目中。当子项目需要使用在父项目dependencyManagement
中声明的依赖时,需在子项目中显式声明该依赖。子项目不指定版本号时,会从父项目的dependencyManagement
部分读取相应依赖的版本号;如果子项目指定了版本号,就使用子项目中指定的版本号,不使用父项目声明的版本。
关于打包方式:父项目的打包方式通常应为 pom
,而不是 jar
或 war
,因为父项目主要起依赖管理和项目聚合的作用,不包含需要部署或运行的代码。
2. 子工程的搭建
在父工程右键选择新建一个 Module,
创建时 Parent 默认为父工程
接着配置子工程的项目依赖,由于父工程已经对版本号进行了统一管理,直接进行以下配置即可
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>
</build>
在一个父项目中搭建两个子工程
然后在这两个子工程中分别写一个查询接口
@RequestMapping("/{orderId}")
public OrderInfo getOrderById(@PathVariable("orderId") Integer orderId){
return orderService.selectOrderById(orderId);
}
@RequestMapping("/{productId}")
public ProductInfo selectProductById(@PathVariable("productId") Integer productId) {
return productService.selectProductById(productId);
}
3. 远程调用
如果此时需要实现一个查询订单的功能,查询结果中需要包含商品信息,由于上面的两个子工程现在是分开的,并不是像之前一样写在一个工程中,那么现在就不能直接调用查询的结果了
可以联想一下前端调用后端获取资源的方式,就是通过发送一个 http 请求来获取的,那么后端之间调用也可以使用这种方式,order-service 服务向 product-service 服务发送一个 http 请求,把得到的返回结果和订单结果合并在一起返回给调用方,有许多方式可以构造一个 http 请求,这里采用 Spring 提供的 RestTemplate 来实现一下:
RestTemplate 是 Spring 提供的,封装 http 调用,并强制使用 RESTful 风格,它会处理 http 连接和关闭,只需要提供资源的地址和参数即可
首先定义一下 RestTemplate
@Configuration
public class BeanConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
之后就可以在 service 中注入 RestTemplate 对象,然后就可以构造 http 请求了,把获取到的资源再通过指定的 ProductInfo 类型进行返回
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public OrderInfo selectOrderById(Integer orderId){
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
//构建URL
String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
//通过get方式发起请求获取资源
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
}
通过这种方式就实现了远程调用
String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
不过上面的方式存在一些弊端,根据 get,post,put 等来区分对资源的操作类型并不能直观地看出来,通过抓包才能看出具体是哪种类型,不如直接把动作放在 URL 上直观
此外,还存在下面的一些问题:
- 由于 IP 地址是固定写的,如果 IP 地址发生了变化就需要修改代码
- 如果是多机部署如何处理?
- 返回结果怎么共用?
- URL 也的书写也容易出现错误
- 接口是对外开放的,存在一定风险
4. 注册中心的引入
针对 IP 地址改变的问题,可以通过下面的流程来解决
- 服务提供者(Server):一次业务中被其它微服务调用的服务,也就是提供接口给其它微服务
- 服务消费者(Client):一次业务中,调用其它微服务的服务,也就是调用其它微服务提供的接口
- 服务注册中心(Registry):用于保存 Server 的注册信息,当 Server 节点发生变更时,Registry 会同步变更,服务与注册中心使用一定机制通信,如果注册中心与某服务长时间无法通信,就会注销该实例
- 服务注册:服务提供者在启动时,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
- 服务发现:服务消费者从注册中心查询服务提供者的地址,并通过该地址调用服务提供者的接口。服务发现的一个重要作用就是提供给服务消费者一个可用的服务列表。
在上面的流程中,通过一个注册中心来存储应用和 IP 之间的关系,服务消费者以此来获取应用的 IP 地址,然后再去远程调用
5. CAP 理论
C:一致性(强一致性),所有节点在同一时间具有相同的数据
当客户端向数据库集群发送了一个数据修改的请求之后,数据库集群需要向客户端进行响应,此时就会有两种情况:
- 主数据库接收到请求并处理成功,不过数据还没有同步到从数据库,随着时间的推移会同步到从数据库
- 主数据库接收到请求,所有的从数据库数据同步成功时
弱一致性就是第一种情况,强一致性就是第二种情况,不论何时,主库和从库对外提供的服务都是一致的
A:可用性,保证每个请求都有响应,不过响应结果可能不对
P:分区容错性,当出现网络分区后,系统仍然能够对外提供服务
这三种特性是不能同时兼顾的,比如,在主数据库和从数据库同步数据的过程中网络出现了问题,那么这个过程就会被拉长,如果保证可用性,那么用户此时获取到的信息就不是强一致性的数据,在微服务架构中, P 是必须要保证的,所以 C 和 A 只能兼顾一个,也就是 CP 架构和 AP 架构
6. Eureka
官方文档:https://github.com/Netflix/eureka/wiki
6.1. 搭建 Eureka Server
首先再创建一个 eureka-server 的子模块
然后引入 eureka-server 的依赖和项目构建的插件
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
接着还需要编写配置文件
server:
port: 10010
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
fetch-registry: false # 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,这里设置为false
register-with-eureka: false # 表示是否将自己注册到Eureka Server,默认为true.由于当前应用就是Eureka Server,故而设置为false.
service-url:
# 设置与Eureka Server的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
然后编写启动类进行启动,和之前的启动类不同的是,这里还需要加上@EnableEurekaServer
注解
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}
启动之后访问服务就可以看到注册中心的界面了:
6.2. 服务注册
把 product-service 注册到 eureka-server 中需要在 product-service 中添加客户端的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
然后配置文件也需要完善一下,把要注册的服务的名称和 eureka 的地址配置一下:
在 product-service 的 yml 文件中添加一下配置,端口号也是之前配置的 eureka 的端口号,
#Eureka Client
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10010/eureka/
然后配置服务名称
spring:
application:
name: product-service
接下来先启动 eureka 服务,再启动 product 服务就能够看到已经注册成功了
然后以相同的方式把 order-service 也添加到注册列表中
6.3. 服务发现
之后再进行远程调用的时候就可以从注册中心来获取要调用服务的 IP 和端口号了,需要在原来获取的方式上进行一些修改
接下来就能成功的获取到 product-service 的信息了
Eureka 和 Zookeeper 的区别:
Eureka 和 Zookeeper 都是用于服务注册和发现的工具,区别有以下几点:
- Eureka 是 Netflix 开源的项目,而 Zookeeper 是 Apache 开源的项目
- Eureka 基于 AP 原则,保证高可用,Zookeeper 基于 CP 原则,保证数据一致性
- Eureka 每个节点都是均等的,Zookeeper 的节点区分 Leader 和 Follower 或 Observer,由于这个原因,如果 Zookeeper 的 Leader 发生故障,需要重新选举,选举过程集群会有短暂时间的不可用