WD
Classnote Docs课程课件
21

21-SpringCloud-01

01 / Section

单体架构

到目前为止,我们所实现的web应用都有一个共同的特点,所有的代码最终打包成一个文件(jar包),整个系统的所有功能单元整体部署到同一个进程,这种软件架构的风格,即所谓的"单体架构"

单体架构的扩容

一个单体应用在运行时,会部署在一台云服务器上,但是随着用户体量的增长,一台云服务器上运行的一个单体应用,已经无法承载日益增长的请求量,怎么办呢?我们可以对单体应用实现扩容,即使用单体应用集群

通过使用单体应用的集群,可以一定程度上,很好的应对日益增长的用户请求,但是这样就完美了吗?当然不是

单体架构的优势和弊端

在单体应用的早期,应用程序相对较小,单体架构的好处:

  • 应用的开发很简单
  • 易于对应用程序进行大规模的更改
  • 测试相对直观简单
  • 部署简单明了
  • 横向扩容不费吹灰之力

但是,随着时间的推移,随着单体应用中包含的功能越来越多,应用的"体积"越来越大,单体应用的弊端就会逐渐体现出来。

  • 代码过度复杂且严重耦合,导致难以维护
java
1. 由于系统本身过于庞大和复杂,以至于任何一个开发者都很难理解它的全部,因此,修复软件中的问题和正确实现新功能就变得困难且耗时
2. 更糟糕的是,这种极度的复杂性,可能会形成一个恶性循环: 由于代码难以理解,因此开发者在更改时更容易出错,每一次更改都会让代码库变得更复杂,更难懂
  • 从代码提交到实际部署的周期很长
text
从代码完成到运行在生产环境,是一个漫长且费力的过程。
1. 众多开发人员都向同一个代码库提交代码,常常使得代码库的构建结构处于无法交付的状态。当采用了分支来解决这个问题,又必须忍受漫长且痛苦的合并过程
2. 因为代码库中的代码十分复杂,以至于任何一个更改可能引起的影响是未知的,为了避免牵一发而动全身的后果,即使是一个微小的更改,也必须执行全部的测试
  • 扩展性受限
text
1. 如果单体应用中的某一个功能点存在性能问题,那么就需要多部署几个单体应用的实例,再加上负载均衡的设备(比如nginx),才能保证整个应用的性能能够支撑用户的使用
2. 在某些情况下,应用的不同模块对资源的需求是相互冲突的,比如某些模块需要高效的IO,某些模块需要高性能的CPU, 而这些模块都在一个单体应用之内,因此其所部署的服务器必须满足所有的需求   
  • 开发慢,启动慢,严重影响开发效率
  • 交付可靠的单体应用困难
text
1. 单体应用体积庞大,难以进行全面和彻底的测试,而缺乏可靠的测试意味着代码中的错误会进入生产环境
2. 缺乏故障隔离,因为所有的模块都在同一个进程中运行,每隔一段时间,在一个模块中的代码错误,将会导致整个应用程序的崩溃
02 / Section

微服务架构

我们看到,随着单体应用的发展,最终会变得难以维护,难以实现及时可靠的交付,且开发效率低。那么怎么解决这个问题呢?解决之道就在于微服务架构。

要理解微服务架构,我们得首先理解微服务,那什么是微服务呢?

简单理解一个微服务的本质就是一个麻雀虽小但五脏俱全的应用程序,它按照单一职责原则实现了特定的一组功能。

  • 因为每个微服务的本质都可以是一个应用程序,这就要求,微服务可以独立部署,独立运行,独立对外提供服务(运行在一个独立的进程中)
  • 每个微服务,根据单一职责原则,实现一组相关功能

在此基础上,什么是微服务架构呢?简单理解,就是把应用程序功能写分解为一组服务的架构风格。实际上,微服务架构是模块化开发的一种形式。

模块化是开发大型,复杂应用程序的基础。当一个单体应用程序的规模太大的时候,是很难作为一个整体开发,也很难让一个人完全理解的。为了让不同的人开发和理解(不同的部分),大型应用需要拆分模块。

  • 在单体架构中,模块通常由一组编程语言所提供的的结构(例如Java中的包,或者jar文件)来定义,但是通过这种方式得到的模块,不同模块的代码还是可以相互引用,导致模块中对象依赖关系的混乱
  • 而微服务架构,使用微服务作为模块化的单元,要访问服务,只能通过服务对外提供的API,于是服务的API为它自身构筑了一个不可逾越的边界,你无法越过API去访问服务内部的类。

微服务的优势和弊端

使用微服务架构,可以解决庞大的单体应用的痛点,带来很多好处:

  • 每个服务都相对较小,容易维护
  • 使得大型的应用程序实现快速的持续交付和持续部署
text
1. 每一个服务相对较小,编写全面的测试代码和执行自动化测试都变得相对容易
2. 每个服务都独立于其他服务部署,如果负责服务的开发人员,需要部署对该服务的更改,不需要与其他开发人员协商,因此将更改频繁部署到生产中要容易的多
  • 应用扩展灵活
text
1. 应用被拆分为不同的微服务,而微服务可以独立部署,因此,扩容就不在针对整个应用了,哪里出现性能瓶颈,对哪个服务扩容即可
2. 即使不同的的服务需要资源存在冲突,也没有关系,把它们分别部署到具有拥有各自所需要资源的机器上即可
  • 更好的容错
text
相比于单体架构中,一个故障拖垮整个系统的情况,一个服务的故障,并不会影响想到其他服务的正常运行。

当然,使用微服务也会带来一些弊端

  • 分布式系统可能复杂难以管理
  • 分布式部署追踪问题难
  • 分布式事务比较难处理
  • 服务数量增加,管理复杂性增加

微服务的拆分

image-20210922143348626
image-20210922143348626

服务拆分的注意事项:

  • 每个服务的功能有边界,因此每个服务访问的数据也是有边界的,所以每个服务都有自己的数据库
  • 每个服务的数据库只限于该服务自己直接访问,其他服务不能直接访问
  • 如果一个服务需要其他服务的数据,则可以通过调用其他服务对外暴露的接口,访问到其他服务的数据

一个微服务架构的项目,大致结构如上图所示,因为每个服务独立运行,独立部署,所以想要将微服务架构在项目中落地,还需要解决一些其他问题,服务之间的调用,服务的治理,比如服务的注册与自动发现,服务调用的负载均衡等等

而这些问题,都由相应的服务框架,已经帮助我们实现了,所以我们在实现微服务架构的项目,都需要基于某个微服务的框架,目前比较流行的有大概有两种SpringCloud和Dubbo,我们的学习主要基于框架SpringCloud。

SpringCloud 基于SpringBoot提供了一套微服务架构实现的解决方案,包括服务的注册与自动发现,面向接口的服务调用,服务调用的负载均衡,服务网关,服务熔断等等组件,它利用SpringBoot开发的便利性,巧妙的简化了分布式系统的基础设施搭建,使开发者可以基于SpringBoot的开发风格做到快速启动和部署。

03 / Section

服务调用的场景

在下单的时候,我们除了需要给用户展示待下单的商品数据,通常我们还需要给用户展示其地址信息,以供其选择。而订单由订单服务管理,用户的地址信息由用户服务管理。

这也就意味着,在下单之前,订单服务不仅需要查询出订单信息,还需要调用用户服务获取用户的地址信息,于是这里就出现了服务调用

04 / Section

服务间的调用

实际上,在基于SpringCloud实现的微服务架构中,一个微服务实例(进程)的本质,就是一个部署在Tomcat中的,满足单一职责原则的应用程序。

服务调用的理论

实际上,服务间的调用,基于我们以前学习过的知识,就可以轻松解决:

  • 基于SpringCloud实现的微服务,支持基于Http协议的通信,因此服务间的调用就变成了一次基于Http的通信
  • 在这次服务调用过程中,调用的目标是谁呢?可以是Controller方法,因为一个Http请求刚好可以被一个Controller方法接收处理,就相当于调用到了这个Controller方法
  • 但是请注意,我们能且只能调用另外一个服务的Controller方法(一个Controller方法就处理一个Http请求,相当于一个服务对外暴露的一个接口),所以说一个服务只能调用另外一个服务对外暴露的接口!

在一次服务调用过程中,我们称调用者为服务消费者(使用或者消费另一个服务的功能),我们称被调用者为服务提供者(提供被消费的功能)

实现服务调用的准备

通过服务调用的理论分析,我们知道服务调用其实就是发送Http请求,接收Http响应的过程,在实现服务调用之前,我们还存在一个问题,就是如何通过代码发送Http请求——使用RestTemplate。

RestTemplate是一个专门用来发送Http请求的工具,通过封装JDK中的HttpURLConnection类库,提供简单易用的模板方法API,它所提供的模板方法几乎覆盖了常用的所有Http请求类型的场景。

我们通常使用其无参构造方法,来获取一个RestTemplate对象:

text
RestTemplate()
Create a new instance of the RestTemplate using default settings.

同时,我们可以通过一个RestTempate对象,发起POST,GET,DELETE,PUT,PATCH等不同种类的Http请求,我们以最常用的GET和POST两种方式为例来学习RestTemplate的用法。

java
/*
	当我们只想要获取一个GET请求的结果的时候,可以调用其getForObject方法,其中
	String url: 发起请求的url
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> T getForObject(String url, Class<T> responseType, Map<String,?> uriVariables)
public <T> T getForObject(String url,Class<T> responseType, Object... uriVariables)

/* 
    如果我们不仅想要获取GET请求的响应体数据,还想获取响应头,以及响应码等信息,那么可以使用getForEntity方法,其中
	String url: GET请求的url
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String,?> uriVariables)
    
java
/*
	当我们只想要获取一个POST请求的结果的时候,可以调用其postForObject方法,其中
	String url: 发起请求的url
	request:表示post请求的请求体数据,也可以不传这个参数
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String,?> uriVariables)
public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
    
/* 
    如果我们不仅想要获取GET请求的响应体数据,还想获取响应头,以及响应码等信息,那么可以使用getForEntity方法,其中
	String url: POST请求的url
	Object request: 表示post请求的请求体数据,也可以不传这个参数
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String,?> uriVariables)

我们以发送GET请求为例来说明:

java
       // 准备好请求的url,携带名为name的请求参数,占位符的名称为name 
	   String url = "http://localhost:9001/user/address/{userId}";
        // map中参数值的key一定要和url中参数的占位符同名
        HashMap<String, Object> param = new HashMap<>();
        param.put("userId", userId);
         //map参数方式
        String rest = restTemplate.getForObject(url, String.class, param);
java
/*
    准备好请求的url,携带名为name的请求参数,占位符的名称就不重要了,
    因为通过可变参数可以通过参数位置确定请求参数对应参数值
*/
String url = "http://localhost:9001/user/address/{userId}";
// 用可变参数的方式来传递请求参数, 这里只有一个参数值name
String rest = restTemplate.getForObject(url,String.class, userId);
java
        // 获取更完整的响应信息
        ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
        // 获取http响应的响应码
        HttpStatus statusCode = forEntity.getStatusCode();
        System.out.println(statusCode);
        // 返回响应体数据
        return forEntity.getBody();

通常我们还可以用

服务调用的实现

被调用接口

那么接下来,我们来实现一个服务消费者调用服务提供者的例子,假设服务提供者对外暴露的接口为

入参 类型
userId Long

请求路径:/user/address/{userId}

请求类型: GET

请求示例:

http://localhost:9001/user/address/{userId}

出参 类型
address String

父工程依赖

因为代码包含多个maven工程,因此我们使用父子工程来实现,创建父工程,父工程中并不包含代码,主要用来管理子Maven工程

xml
<properties>
  <java.version>17</java.version>
  <spring-boot.version>2.7.17</spring-boot.version>
  <spring-cloud.version>2021.0.9</spring-cloud.version>
  <spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
  <mysql.version>8.0.20</mysql.version>
  <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
  <lombok.verion>1.18.24</lombok.verion>
</properties>
<dependencyManagement>
  <dependencies>
    <!-- spring-boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <!--spring cloud alibaba-->
    <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>

    <!--spring cloud-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <!-- mysql驱动 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>${mysql.version}</version>
    </dependency>
    <!--mybatis-plus-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>${mybatis-plus.version}</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>${lombok.verion}</version>
    </dependency>
  </dependencies>
</dependencyManagement>

服务提供者实现

依赖如下

xml
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

配置如下

yml
server:
  port: 9001
spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3306/demo_user?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver

代码如下(SpringBoot工程的启动类就不展示了)

java
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    
    /**
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/address/{id}")
    public String queryById(@PathVariable("id") Long id) {
        // 根据id查询用户的地址信息
        return userService.queryById(id);
    }
}
java
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public String queryById(Long id) {
        User user = userMapper.findById(id);
        return user.getAddress();
    }
}

服务消费者实现

依赖如下

xml
 	 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

配置如下

yml
server:
  port: 9002
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3306/demo_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver

代码如下

先准备好一个用于发起Http请求的RestTemplate对象

java
@Configuration
public class ClientConfig {

    @Bean
    public RestTemplate template() {
        return new RestTemplate();
    }
}
java
@RestController
@RequestMapping("order")
public class OrderController {

   @Autowired
   private OrderService orderService;

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }
}
java
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;


    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1.url地址
        String url = "http://localhost:9001/user/address/{userId}";
        // 2.2.发送http请求,实现远程调用
        String userAddress = restTemplate.getForObject(url, String.class,order.getUserId());
        // 3.封装user到Order
        order.setUserAdress(userAddress);
        // 4.返回
        return order;
    }
}
05 / Section

服务的注册与发现

服务间调用的问题,但是仅仅实现到这种程度,还远远不够

问题的引出

首先回顾一下,对于服务消费者而言,它如何知道服务提供者的信息,从而调用远程服务的功能呢? 再来看看代码

java
String url = "http://localhost:9001/user/address/{userId}";
// 实现远程调用
String userAddress = restTemplate.getForObject(url, String.class, order.getUserId());

但是,将服务消费者要调用的服务提供者地址写死,这样好吗?我们来设想如场景

  • 假设对于用户服务的请求量很大,一个用户服务实例(进程)处理不了这么多的请求了,此时我们就可以启动多个用户服务实例(进程),此时,这多个用户服务实例就组成了用户服务的集群
  • 但是我们如果写死了调用的服务提供者的地址,即使有用户服务的集群,有意义吗?没有,因为写死了服务调用的地址,所以我们永远只能调用到集群中的一个服务实例!

此时矛盾就出现了,服务调用时必须知道服务提供者的地址,但是在代码里把改地址写死,就永远只能调用确定的那一个服务实例,但是如果不在代码里写服务调用的地址,我们又从哪里得到服务提供者实例的地址呢?

服务注册中心

如果在定义服务消费者的时候,不指明服务消费者调用的服务提供者地址,那么服务消费者怎么知道去哪里调用服务提供者呢?此时,我们就需要引入一个新的角色——服务注册中心,由服务注册中心来统一管理服务的状态和信息,那么这个问题就可以解决了。

服务注册中心 (1)
服务注册中心 (1)

对于每一个服务

  • 在服务启动时,会将自己的信息,注册到服务注册中心,其中就包括,ip地址,端口号等信息。即实现服务的注册
  • 在服务运行过程中,会时时向服务注册中心"报告"自己的状态,因此服务注册中心就可以实时感知到服务的运行状态
  • 同时,在服务启动时,也会去注册中心拉取,其他服务信息,即将服务注册表信息下载到本地,这样一来一个服务就可以知道,其他服务调用地址等信息
  • 在服务运行的过程中,服务会从注册中心时时拉取最新的服务注册表信息,从而实现服务的实时发现

那么服务注册中心需要我们自己去实现吗?不是,已经有很多的注册中心实现供我们使用了,比如SpringCloud Alibaba中的Nacos,以及SpringCloud Netflix中的Eureka等等

Nacos 注册中心

Nacos(Dynamic Naming and Configuration Service)是SpringCloud Alibaba中包含的注册中心组件,实现服务的注册与自动发现功能,Nacos主要采用C-S架构实现

  • NacosServer实现注册中心的功能,可以直接独立运行
  • NacosDiscovertyClient负责帮助服务实例访问NacosServer,实现服务的注册和自动发现

启动Nacos Server

首先,下载好Nacos安装包(.zip压缩包),解压后,如下图

进入bin目录,打开命令行,输入如下命令

text
windows: startup.cmd -m standalone
linux 或 mac: startup.sh -m standalone

启动之后看到如下界面,这里上面的部分没截全

nacos启动
nacos启动

启动Nacos Server之后,我们就可以通过localhost:8848/nacos 访问Naocs自带的控制台查看注册中心了。 用户名和密码 nacos

下面我们使用Nacos作为服务注册中心,实现服务的注册与自动发现。

服务的注册

首先改造用户服务和订单服务。

在用户服务中,添加如下依赖

xml
<!--Nacos注册中心客户端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

添加如下配置指定Nacos Server地址

yaml
spring:
  cloud:
    nacos:
      discovery:
       # nacos server地址
        server-addr: localhost:8848 

在启动类上加注解@EnableDiscoveryClient

java
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.cskaoyan.user.mapper")
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

用户服务的其他代码不需要做任何改变

在订单服务中,添加如下依赖

xml
<!--Nacos注册中心客户端-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

在订单服务中,添加如下配置

yml
spring:
  cloud:
    nacos:
      discovery:
       # nacos server地址
        server-addr: localhost:8848
        

在启动类上加注解@EnableDiscoveryClient

java
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.cskaoyan.order.mapper")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

做完以上工作之后,我们只需要分别启动用户服务和订单服务,查看Nacos Server

服务的发现

在服务成功注册到服务注册中心之后,我们如何动态的发现服务的信息,获取到服务运行所在的IP地址和端口号,从而根据服务的IP和端口号,利用RestTemplate发起服务调用请求呢,代码如下:

java
// 注入服务发现的客户端对象,通过该对象访问从注册中心下载的服务信息 
    @Autowired
    DiscoveryClient discoveryClient; 

    public Order queryOrderById(Long orderId) {
         // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        // 2. 调用用户服务
        // 2.1 获取指定服务名称的服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 2.2 选择服务实例,获取uri(格式为http://服务实例ip:服务实例启动端口)
        URI uri = instances.get(0).getUri();
	    // 2.3 向用户服务发起服务调用请求(注意,这里没有写死ip地址和端口号了!!!)
        ResponseEntity<String> response = template.getForEntity(uri.toString() + "/user/address/{userId}", String.class, order.getUserId());
        String result = response.getBody();
        return result;
    }

服务集群

其实,我们上面所说的运行一个服务,严格来说,运行的应该是一个服务实例,对应的是一个独立的JAVA进程。每个服务实例都有自己所对应的服务名称,多个服务实例运行相同的代码,且具有相同的服务名称,我们就称这些服务实例组成了一个服务集群。比如,我们可以把同一个user-service工程启动两次,那么我们就有了两个用户服务实例,它们组成了一个用户服务集群。

本来没什么可说的,但是在上课期间需要在同一台电脑上启动user-service工程启动两次,它们使用相同的代码,相同的配置文件,直接启动会出现端口冲突,所以我们需要学习在IDEA中如何实现该功能。

先修改第一个用户服务实例的启动脚本名称

紧接着,将第一个用户服务实例的启动脚本复制一份,作为第二个用户服务实例的启动脚本并修改启端口为9003

Eureka 注册中心(了解)

Eureka是SpringCloud Netflix中包含的注册中心的组件,Eureka也采用了C-S的架构设计,其中

  • EurekaServer作为服务器端,它具体就实现了服务注册中心的功能
  • EurekaClient作为客户端,帮助服务实例提完成服务的注册与自动发现

接下来,我们就基于Eureka服务注册中心,实现服务的注册与自动发现。

配置并启动EurekaServer

与nacos不同,我们需要新建一个子eureka-server工程,这个工程仅仅只是为了启动一个EurekaServer进程(注册中心)

在该工程中添加依赖

java
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

在eureka-server子工程中的application.yml配置文件中添加如下配置

yml
server:
  port: 7001
# Eureka配置
eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: localhost
  client:
    # 表示是否向 Eureka 注册中心注册自己(这个模块本身是服务器,所以不需要)
    register-with-eureka: false
    # fetch-registry如果为false,则表示自己为注册中心,客户端的化为 ture
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:7001/eureka/

启动类上添加注解@EnableEurekaServer

java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServer.class, args);
    }
}

一旦运行eureka-server工程,我们就可以通过 localhost:7001(这里的端口号就是我们server.port配置的端口)访问Eureka自带的控制台了

服务注册

在user-service的Maven工程中原有依赖的基础上,注释nacos-discovery依赖,添加如下依赖

xml
        <!--Eureka Client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

添加如下配置

text
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: user-instance-1 
    hostname: localhost

主启动类添加注解@EnableEurekaClient

java
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.cskaoyan.user.mapper")
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

在order-service的Maven工程中原有依赖的基础上,注释nacos-discovery依赖,添加如下依赖

xml
      
<!--Eureka Client依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

添加如下配置

text
# Eureka配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: order-instance
    hostname: localhost

主启动类,添加注解

java
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.cskaoyan.order.mapper")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

接下来,我们只需要分别启动用户服务实例和订单服务实例,即可观察到服务注册效果

服务发现

在服务消费者的OrderService中添加测试代码

java
// 注入服务发现的客户端对象,通过该对象访问从注册中心下载的服务信息 
    @Autowired
    DiscoveryClient discoveryClient; 

    public Order queryOrderById(Long orderId) {
         // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        // 2. 调用用户服务
        // 2.1 获取指定服务名称的服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 2.2 选择服务实例,获取uri(格式为http://服务实例ip:服务实例启动端口)
        URI uri = instances.get(0).getUri();
	    // 2.3 向用户服务发起服务调用请求(注意,这里没有写死ip地址和端口号了!!!)
        ResponseEntity<String> response = template.getForEntity(uri.toString() + "/user/address/{userId}", String.class, order.getUserId());
        String result = response.getBody();
        order.setAddress(result);
        return order;
    }

服务集群

在Eureka 中我们同样也可以有多个服务实例组成的服务集群,通过相同的方式,我们同样可以启动两个用户服务实例,并向Eureka注册中心注册,但是对于第二个服务实例,我们需要再启动脚本中,在添加一个instance-id

Eureka的自我保护机制

自我保护机制触发的场景如下:

  • 默认情况下,当eureka server在一定时间内没有收到服务实例的心跳,便会把该实例从注册表中删除(默认是90秒
  • 但是,如果短时间内丢失大量的服务实例心跳数据,这意味着短时间内大量的服务连接丢失了,此时就会触发Eureka的自我保护机制
  • 触发自我保护机制的结果就是,Eureka认为虽然收不到实例的心跳,但它认为实例还是健康的,eureka会保护这些实例,不会把它们从注册表中删掉。

那么Eureka的自我保护机制的意义在哪里呢?

  • 该保护机制的目的是避免网络连接故障,在发生网络故障时,微服务和注册中心之间无法正常通信,但服务本身是健康的
  • 此时,为了避免注册中心同时删除大量本来是正常运行(健康的)的服务实例,于是就会自动触发自我保护机制
  • 这样一来,即使注册中心和某个或者某些服务实例的网络出现问题,其他服务实例还是可以通过注册中心,获取到其地址,正常发起调用请求

但是我们在开发测试阶段,需要频繁地重启发布,如果触发了保护机制,则旧的服务实例没有被删除,这时服务消费者按照注册表中的服务提供者信息,发出服务调用请求,会因为该实例关闭而失败,这就导致请求错误,影响开发测试。

所以,在开发测试阶段,我们可以把自我保护模式关闭,只需在eureka server配置文件中加上如下配置即可

text
eureka:
  server:
    enable-self-preservation: false