SpringCloud学习笔记

1 微服务

1.1 什么是微服务

  • 微服务是一种 软件开发技术,而微服务架构是一种 架构模式,两者不可混为一谈

  • 微服务提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值

  • 每个服务运行在独立的进程中,服务与服务间采用轻量级的通信机制互相沟通

  • 每个服务都围绕着具体业务进行构建,并且能够独立地部署到生产环境、类生产环境等

  • 应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建

  • 类比一下,一个微服务就相当于 Java 工程中的一个 Model

1.2 微服务优缺点

微服务优点

  • 每个服务足够内聚,足够小,代码容易理解,聚焦于一个指定的业务或者业务需求

  • 开发简单、开发效率高,一个服务可能就是专一的只干一件事

  • 它的结构是松耦合的,是具有具体功能和意义的服务,在开发的整个阶段都是相对独立的

  • 微服务架构他只是一个思想,对于开发语言没有任何限制

  • 容易通过 Jebkins、Hudson、bamboo 等持续集成工具与第三方集成

  • 微服务只是业务逻辑的代码,不会与前端的页面混合

  • 每个微服务都有自己的存储能力,可以使用自己的独立数据库,也可以有统一数据库

微服务缺点

  • 开发人员需要去处理分布式系统的复杂性

  • 随着服务的增加,运维的难度也在增加

  • 系统依赖、数据一致性、性能监控

  • 各个服务之间通信成本、系统部署的依赖问题

1.3 微服务技术栈

微服务项目 技术栈
服务开发 Spring、SpringMVC、SpringBoot
服务配置与管理 Archaius、Diamond
服务注册与发现 Eureka、Consul、Zookeeper
服务调用 Rest、RPC、gRPC
服务熔断器 Hystrix、Envoy
负载均衡 Ribbon、Nginx
服务接口调用 Feign
消息队列 Kafka、RabbitMQ、ActiveMQ
服务配置中心管理 SpringCloudConfig、Chef
服务路由(API网关) Zuul
服务监控 Zabblx、Nagious、Metrics、Specatator
全链路追踪 Zipkin、Brave、Dapper
服务部署 Docker、OpenStack、Kubernetes
数据流操作开发包 SpringCloud Stream(封装 Redis、Rabbit、Kafka)
事件消息总线 SpringCloud Bus

2 SpringCloud 简介

image-20220625204804045

2.1 什么是 SpringCloud

  • SpringCloud 为开发者提供了一套微服务解决方案,包括服务注册与发现、配置中心、全链路监控、服务网关、负载均衡、熔断器等组件

  • SpringCloud 巧妙的简化了分布式系统基础设施的开发,为开发人员提供了 配置管理、服务发现、断路器、路由、微代理、时间总线、全局锁、决策竞选、分布式会话 一些列快速构建分布式系统的工具

  • SpringCloud 是分布式微服务架构之下的一站式解决方案,是各个微服务架构落地技术的集合体

2.2 与 SpringBoot 的关系

  • SpringBoot 专注于快速方便地开发单个个体微服务

  • SpringCloud 是专注于全局的微服务之间的协调和治理,为各个微服务模块之间提供集成服务

  • SpringBoot 可以离开 SpringCloud 独立开发项目,但是 SpringCloud 却离不开 SpringBoot,它依赖于 SpringBoot 进行开发

  • SpringBoot 专注于快速、方便地开发 单体微服务,SpringCloud 专注于 全局的微服务治理

2.3 分布式 + 服务治理

目前成熟的大型网站,互联网架构一般为:应用服务化拆分 + 消息中间件

image-20220625220152243

3 分布式 Demo

由此开始的所有学习,均基于这个 Demo 进行

创建一个父工程,专门用来管理依赖,后续的功能直接添加模块即可。父工程依赖如下

<?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>com.jiuxiao</groupId>
    <artifactId>spring-cloud-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>spring-cloud-api</module>
    </modules>

    <packaging>pom</packaging>

    <properties>
        <project.bulid.sourceEncoding>UTF-8</project.bulid.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.cloud.version>2021.0.3</spring.cloud.version>
        <spring.boot.version>2.6.4</spring.boot.version>
        <mysql.version>8.0.29</mysql.version>
        <druid.version>1.2.11</druid.version>
        <mybatis.springboot.version>2.2.2</mybatis.springboot.version>
        <junit.version>4.13.2</junit.version>
        <lombok.version>1.18.22</lombok.version>
        <log4j.version>1.2.17</log4j.version>
        <logback.core.version>1.2.11</logback.core.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--SpringCloud依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringBoot依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--Mysql依赖-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--数据源druid依赖-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <!--SpringBoot-Mybatis启动器-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.springboot.version}</version>
            </dependency>
            <!--junit依赖-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <!--lombok依赖-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <!--log4j依赖-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <!--日志门面依赖-->
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>${logback.core.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

3.1 环境搭建:服务提供者

1 spring-cloud-api 模块

  1. 创建第一个子项目 spring-cloud-api,只导入 lombok 依赖

  2. 新建一个数据库,名为 db01 ,在该数据库中建立一张部门表(dept),建表语句如下

create database `db01`;

use `db01`;

create table `dept`(
    `dept_id` int(32) not null primary key auto_increment comment '部门编号',
    `dept_name` varchar(150) not null comment '部门名称',
    `db_source` varchar(150) not null comment '该资源所存储的数据库'
) comment '部门表';

insert into db01.dept (dept_name, db_source) value ('开发部', database());
insert into db01.dept (dept_name, db_source) value ('人事部', database());
insert into db01.dept (dept_name, db_source) value ('财务部', database());
insert into db01.dept (dept_name, db_source) value ('市场部', database());
insert into db01.dept (dept_name, db_source) value ('运维部', database());

image-20220626105450548

  1. 创建于数据库表对应的实体类 Dept ,这里需要将实体类进行序列化,实体类完成之后,该模块结束
/**
 * 部门实体类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 11:00
 * @Version: 1.0.0
 */
@Data
@NoArgsConstructor
@Accessors(chain = true)    //开启链式写法
public class Dept implements Serializable {

    /** 部门id */
    private Integer deptId;

    /** 部门姓名 */
    private String deptName;

    /** 该数据来自于哪个数据库,微服务阶段,一个服务对应一个数据库,一个信息可能存在于不同数据库 */
    private String dbSource;

    public Dept(String deptName) {
        this.deptName = deptName;
    }
}

2 spring-cloud-provider-dept-8001 模块

  1. 创建新的子模块(端口号:8001),该模块为部门提供者,在依赖配置中,我们需要拿到 spring-cloud-api 模块的实体类
<!--api模块依赖-->
<dependency>
    <groupId>com.jiuxiao</groupId>
    <artifactId>spring-cloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!--junit依赖-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
<!--mysql依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid数据源依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--logback依赖-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Springboot测试依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
</dependency>
<!--jetty依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<!--Web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--热部署依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
  1. 接下来去写该模块的配置文件
# 服务配置
server:
  port: 8001

# MyBatis配置
mybatis:
  mapper-locations: classpath:mybatis/mapper/*.xml
  type-aliases-package: com.jiuxiao.springcloud.pojo
  configuration:
    map-underscore-to-camel-case: true

# Spring 配置
spring:
  application:
    name: spring-cloud-provider-dept
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
    username: root
    password: '0531'
    driver-class-name: com.mysql.cj.jdbc.Driver
  1. 编写 dao 层的接口类
/**
 * 部门Dao层接口
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 11:47
 * @Version: 1.0.0
 */
@Mapper
@Repository
public interface DeptDao {

    /**
     * @param id
     * @return: com.jiuxiao.springcloud.pojo.Dept
     * @decription 根据 ID 查询部门
     * @date 2022/6/26 11:49
     */
    Dept queryDeptById(Integer id);

    /**
     * @return: java.util.List<com.jiuxiao.springcloud.pojo.Dept>
     * @decription 查询所有部门
     * @date 2022/6/26 11:50
     */
    List<Dept> queryAllDept();
}
  1. 编写 Dao 层对应的 Mapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jiuxiao.springcloud.dao.DeptDao">

    <!--根据 ID 查询部门-->
    <select id="queryDeptById" parameterType="int" resultType="Dept">
        select dept_id, dept_name, db_source
        from db01.dept
        where dept_id = #{id}
    </select>

    <!--查询所有部门-->
    <select id="queryAllDept" resultType="Dept">
        select dept_id, dept_name, db_source
        from db01.dept
    </select>
</mapper>
  1. 然后是对应的 Service 层、Service 实现类、Controller 层
/**
 * 部门业务层接口
 * @Author: 悟道九霄
 * @Date: 2022/06/26 14:36
 * @Version: 1.0.0
 */
@Service
public interface DeptService {

    /**
     * @param id
     * @return: com.jiuxiao.springcloud.pojo.Dept
     * @decription 根据 ID 查询部门
     * @date 2022/6/26 11:49
     */
    Dept queryDeptById(Integer id);

    /**
     * @return: java.util.List<com.jiuxiao.springcloud.pojo.Dept>
     * @decription 查询所有部门
     * @date 2022/6/26 11:50
     */
    List<Dept> queryAllDept();
}
/**
 * 部门业务层实现类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 14:36
 * @Version: 1.0.0
 */
@Service("DeptService")
public class DeptServiceImpl implements DeptService{

    @Resource
    private DeptDao deptDao;

    @Override
    public Dept queryDeptById(Integer id) {
        return deptDao.queryDeptById(id);
    }

    @Override
    public List<Dept> queryAllDept() {
        return deptDao.queryAllDept();
    }
}
/**
 * 部门控制类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 14:39
 * @Version: 1.0.0
 */
@RestController("/dept")
public class DeptController {

    @Resource
    private DeptService deptService;

    @GetMapping("/get/{id}")
    public Dept queryDeptById(@PathVariable("id") Integer id){
        return deptService.queryDeptById(id);
    }

    @PostMapping("/list")
    public List<Dept> queryAllDept(){
        return deptService.queryAllDept();
    }
}
  1. 写一个主启动类
/**
 * 启动类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 14:52
 * @Version: 1.0.0
 */
@SpringBootApplication
public class DeptProvide8001 {

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

启动项目,测试几个接口,均成功后,服务提供者的环境搭建到此结束

3.2 环境搭建:服务消费者

  1. 创建一个服务消费者子项目(端口号:80),导入依赖
<!--实体类api依赖-->
<dependency>
    <groupId>com.jiuxiao</groupId>
    <artifactId>spring-cloud-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!--Web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--热部署依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
  1. 编写配置文件
server:
  port: 80
  1. 该模块作为消费者,是没有 Service 层的,那么在 Controller 中怎么获得要请求的 url ?

我们想到了 SpringBoot 中一个特殊的东西:模板(xxxTemplate

考虑到 SpringBoot 中很多都是 Restful 风格的请求,所以我们全局搜索 RestTemplate

在该类中,查看所有方法,我们发现,他有一些方法的开头是 get、post、put、delete ,这不正对应着请求的方式?

image-20220626154056704

不出意外的话,就要使用这些方法获取请求的 url 了,那么首先,我们应该将它注册为 bean

  1. 新建一个配置类,将 RestTemplate 注册为 Bean
/**
 * Bean配置类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 15:46
 * @Version: 1.0.0
 */
@Configuration
public class ConfigBean {

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 在 Controller 模仿消费者可客户端,进行数据获取
/**
 * 部门服务消费者控制器
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 15:29
 * @Version: 1.0.0
 */
@RestController
public class DeptConsumerController {

    @Resource
    private RestTemplate restTemplate;

    private static final String REST_URL_PREFIX = "http://localhost:8001";

    @RequestMapping("/consumer/dept/get/{id}")
    public Dept queryDeptById(@PathVariable("id") Integer id){
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
    }

    @RequestMapping("/consumer/dept/list")
    public List<Dept> queryAllDept(){
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
    }
}
  1. 启动项目,现在生产者端进行访问(8001端口)

image-20220626162534692

image-20220626163141906

然后再去消费者端,模拟用户进行访问(80端口)

image-20220626162640898

image-20220626163206753

完全不同的 url,完全相同的结果,到此为止,微服务的基本环境搭建完成

4 Eureka

4.1 什么是 Eureka

  • Netflix 在设计 Eureka 时,遵循的就是 AP 原则,它是 Netflix 的核心模块之一

  • Eureka 是一个基于 REST 的服务,用于定位服务,用来实现云端中间层服务的发现和故障转移。只需要使用服务中心的标识符就可以访问到服务,而不用去修改配置文件,该功能类似于 Dubbo 的注册中心

  • SpringCloud 封装了 Netflix 公司开发的 Eureka 模块,来实现服务注册和发现

  • Eureka 采用了 C-S 架构设计,使用 EurekaServer 作为服务注册功能的服务器,也即注册中心

  • 系统中的其他微服务,使用 Eureka 的客户端连接到 EurekaServer 并维持心跳连接(每隔一段时间通信一次,若有响应,则证明连接存活;否则表示连接死亡)

  • SpringCloud 的一些其他模块,可以通过 EurekaServer 来发现系统中的其他微服务,并执行与之对应的逻辑

4.2 Eureka 原理

  • Eureka 包含两个组件:Eureka ServerEureka Client

  • Eureka Server 提供服务注册服务,各个节点启动之后,会在 Eureka Server 中进行注册,服务注册表中会存储所有可用的服务结点信息

  • Eureka Client 是一个 Java 客户端,用于简化 Eureka Server 的交互,客户端同时也具备一个内置的、使用轮询负载算法的负载均衡器

  • Eureka Client 会在应用启动之后,,每隔一段时间(默认 30 秒)会像 Eureka Server 发送心跳,若有回复则表示依旧在保持连接;否则 Eureka Server 会将该服务节点从服务注册表中移除掉(默认周期为 90 秒)

image-20220626213813320

4.3 Eureka 的三大角色

  • Eureka Server:提供服务的注册与发现(类似于 Zookeeper)

  • Eureka Provider:将自身的服务注册到 Eureka 中,从而使得消费方能够找到

  • Eureka Consumer:服务消费方从 Eureka 中获取注册服务列表,从而找到消费服务

4.4 EurekaServer 搭建

  1. 依旧在上述的父项目中新建子项目(端口号:7001),导入 Eureka Server 的依赖
<!--Eureka Server依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
<!--热部署依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
  1. 编写配置文件
server:
  port: 7001

# Eureka 配置
eureka:
  instance:
    # Eureka服务端的实例名称
    a-s-g-name: localhost
  client:
    # 是否向 Eureka 注册中心注册自己(这里是服务端,所以为 false)
    register-with-eureka: false
    # false 表示自己为注册中心
    fetch-registry: false
    # 服务地址(监控页面):Eureka 默认的端口号为 8761,这里需要使用我们自己的
    service-url:
      defaultZone: http://${eureka.instance.a-s-g-name}:${server.port}/eureka/
  1. 启动类上加上 @EnableEurekaServer ,即可开启 Eureka 服务
/**
 * 启动类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 22:24
 * @Version: 1.0.0
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer7001 {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServer7001.class, args);
    }
}
  1. 启动项目,访问我,我们配置的 7001 端口,Eureka 环境搭建成功

image-20220626223140586

4.5 注册 Eureka 服务

上面已经写好了 Eureka 服务,现在需要将 Eureka 服务注册到服务提供者中(端口:8001)

  1. 首先在服务提供者(端口:8001)模块中,导入 Eureka 依赖,尽量与 7001 模块中的 Eureka Server 版本一致
<!--Eureka依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
  1. 然后去 yml 中配置 Eureka
# Eureka 配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  1. 最后就是去启动类上使用注解开启 Eureka 服务
/**
 * 启动类
 * @Author: 悟道九霄
 * @Date: 2022年06月26日 14:52
 * @Version: 1.0.0
 */
@SpringBootApplication
@EnableEurekaClient
public class DeptProvide8001 {

    public static void main(String[] args) {
        SpringApplication.run(DeptProvide8001.class, args);
    }
}
  1. 依次启动 7001 的 Eureka Server、 8001 的 Eureka Client 服务,两个服务启动之后,访问 4.4 中 Eureka 的监控页面,可以发现已经监控到了 8001 端口的服务

image-20220627095517369

这里的 Application 名称为 SPRING-CLOUD-PROVIDER-DEPT ,对应的就是我们在 8001 的 yml 配置文件中配置的 spring.application.name

spring:
  application:
    name: spring-cloud-provider-dept

4.6 信息、自我保护机制

4.6.1 信息配置

使用 instance-id 配置,可以修改 Eureka 监控页面的默认 Status 信息

eureka:
  instance:
    instance-id: spring-cloud-prod-dept-8001

image-20220627101021898

4.6.2 自我保护机制

我们将两个端口的服务均启动之后,将 8001 端口的服务手动结束,模拟网络故障,这个时候再去 Eureka 监控面板

8001 的服务依旧被保留,面板上方出现了红色的提示信息(紧急情况! EUREKA 可能不正确地声称实例已启动,但实际上并未启动。 续订少于阈值,因此为了安全起见,实例不会过期)

image-20220627152419256

  • 某一时刻一个微服务不可用了,Eureka 并不会立刻清理,依旧会对该服务进行信息保存

  • 默认情况下,若 Eureka 在 90 秒内没有接收到某个微服务实例的心跳,Eureka Server 将会对该实例进行注销。但是,当网络故障等不可控情况发生时,Eureka 无法与微服务实例之间实现通信,但是并不是该服务实例的问题,这个时候 Eureka Server 就会进入自我保护模式,在该模式中,不再会自动删除注册表中的数据(不会销毁任何实例),当网络故障恢复之后,该 Eureka Server 结点就会自动退出自我保护模式,恢复正常

  • 该机制的思想就是:宁可保留不健康的微服务,也不会盲目的注销任何一个健康的微服务

image-20220627152023057

4.6.3 获取微服务信息

在与他人团队协作的时候,我们需要获得他人的微服务相关的信息,Eureka 为我们提供了这一功能

在 8001 的 Controller 中,我们定义一个请求来获取微服务信息

@RestController
public class DeptController {
    
    @Resource
    private DiscoveryClient client;
    
    //获取指定的微服务信息
    @GetMapping("/dept/discovery")
    public Object discovery(){
        List<String> services = client.getServices();
        System.out.println("discovery==>services : " + services);
		//这里的参数为微服务名称,即 ApplicationName
        List<ServiceInstance> instances = client.getInstances("SPRING-CLOUD-PROVIDER-DEPT");
        for (ServiceInstance instance : instances) {
            System.out.println("Host : " + instance.getHost());
            System.out.println("Port + " + instance.getPort());
            System.out.println("Uri : " + instance.getUri());
            System.out.println("ServiceId : " + instance.getServiceId());
        }
        return this.client;
    }
}

然后需要在启动类上加一个注解 @EnableDiscoveryClient 来开启发现客户端的功能

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class DeptProvide8001 {

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

启动 8001,访问我们配置的请求路径 http://localhost:8001/dept/discovery,前端输出了该微服务的相关信息,控制台同时也清晰打印了出来

image-20220627155955809

image-20220627155944017

4.7 集群环境配置

image-20220627221307108

  1. 现在搭建一个小型集群,建立两个新的模块,端口号分别为 7002、7003,配置文件(只需修改端口号)以及依赖均和 7001 一致

  2. 前边在 Eureka 配置中,只关联了一个结点,但是要做集群,每个结点都要映射另外几个,这应该怎么做?很简单,在配置中直接将另外几个直接挂载

  3. 首先要去计算机的 hosts 文件中,进行域名映射,否则无法获取另外的 Eureka 结点(测试完之后记得删除,系统的 hosts 文件不要乱修改)

127.0.0.1       www.jiuxiao.com
127.0.0.1       www.huang.com
127.0.0.1       www.wudao.com
# 7001 的配置
eureka:
  client:
    service-url:
      defaultZone: http://www.huang.com:7002/eureka/,http://www.wudao.com:7003/eureka/
  
# 7002 的配置
eureka:
  client:
    service-url:
      defaultZone: http://www.jiuxiao.com:7001/eureka/,http://www.wudao.com:7003/eureka/
  
# 7003 的配置
eureka:
  client:
    service-url:
      defaultZone: http://www.jiuxiao.com:7001/eureka/,http://www.huang.com:7002/eureka/
  1. 现在 7001、7002、7003 三个集群已经建立完毕,接下来要去修改 8001,因为我们要将服务发布到集群中去,修改配置文件即可
# 8001 的配置
eureka:
  client:
    service-url:
      defaultZone: http://www.jiuxiao.com:7001/eureka/,http://www.huang.com:7002/eureka/,http://www.wudao.com:7003/eureka/
  1. 首先依次启动三个集群,最后再启动提供者 8001 端口,当我们访问 7001 对应的监控页面时,已经出现了另外两个集群,并且监控着 8001 实例

image-20220627220217913

  1. 此时,我们将 7002 端口手动结束来模拟不可抗意外导致的集群错误,访问 7001 或者 7003,可以看到,服务仍然正常

image-20220627220629426

4.8 对比 Zookeeper

4.8.1 CAP 原则

什么是 CAP ?

  • Consistency :强一致性

  • Availablity :可用性

  • Partition Tolerance :分区容错性

CAP 原则总是遵循三进二原则:永远只会满足上面三个原则中的两个,不可能全部满足

什么是 ACID ?

  • Atomicity :原子性

  • Consistency :一致性

  • Isolation :隔离性

  • Durability :持久性

数据库可以分为关系型和非关系型:关系型数据库(Mysql、Oracle、SqlServer)遵循 ACID 原则,非关系型数据库(Reids、mongDB)遵循 ACP 原则

4.8.2 Eureka VS Zookeeper

CAP 理论指出:一个分布式系统不可能同时满足一致性(C)、可用性(A)、容错性(P)三个原则

在分布式系统中,分区容错性(P)是必须要保证的,因此只能在 A 和 C 之间进行权衡

Zookeeper 保证了 CP 原则

image-20220628104208556

  • 向注册中心查询服务列表时,我们可以忍受注册中心返回的是几分钟之前的注册信息,但不能接受服务直接 down 掉不可用

  • 服务注册的可用性要求高于一致性

  • master 结点因为网络故障与其他结点失去联系时,剩余结点会重新进行 leader 选举

  • 但是时间过长(30s ~ 120s),并且选举期间整个 zookeeper 集群是不可用的,这就导致在选举期间注册服务瘫痪

  • 虽然服务最终能恢复,但是漫长的选举时间导致注册长时间不可用这是不能容忍的(可用性 ↓)

Eureka 保证的是 AP 原则

image-20220628105750294

  • Eureka 在设计之初就可以先保证可用性(A)

  • Eureka 各个节点都是平等的,几个节点 down 掉并不会影响正常节点的工作,剩余结点依旧可以提供注册和查询服务

  • Eureka Client 在向某个 Eureka Server 注册时,如果连接失败,则会自动切换至其他节点,只要有一台 Eureka Server 还存活,节能保证注册服务的可用性,只不过查询的信息可能不是最新的(可用性 ↑)

  • Eureka 还有 自我保护机制,若 15 分钟内超过 85% 的节点均无正常心跳,那么 Eureka 认为客户端与注册中心出现了网络故障,此时会有以下情况:

    • Eureka 不再从注册表中移除因长时间未收到心跳而过期的服务

    • Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点之上(保证当前节点依然可用)

    • 当网络稳定时,当前实例新的注册信息会被同步到其他节点中

总结:Eureka 可以很好地应对因为网络故障而导致的部分节点失联的情况,而不会像 zookeeper 那样直接让整个服务瘫痪

5 Ribbon

5.1 什么是 Ribbon

  • Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套 客户端负载均衡的工具

  • 它的主要功能就是为客户端的软件提供负载均衡的算法,将 Netflix 的中间层服务连接在一起

  • 在配置文件中列举出 LoadBalancer(简称LB:负载均衡)后面的所有机器,Ribbon 会自动基于某种规则(轮询、随机)去连接这些机器

5.2 Ribbon 作用

  • 负载均衡简单来说就是将用户的请求平均分摊到多个服务器中,从而达到系统的高可用

  • 常见的负载均衡软件有 Nginx、LVS 等

  • Dubbo 和 SpringCloud 中均提供了负载均衡,并且 SpringCloud 的负载均衡算法可以自定义

负载均衡简单分类:

  • 集中式负载均衡:在服务的消费方和提供方之间使用独立的负载均衡设施(Nginx),该设施负责将请求通过某种策略转发至微服务的提供方

  • 进程式负载均衡

    • 将负载均衡逻辑集成到消费方,消费方从服务注册中心获取可用的地址,然后从这些地址中选一个合适的服务器

    • Ribbon 就属于进程内负载均衡,他只是一个类库。集成于消费方进程中,消费方通过他来获取到服务提供方的地址

5.3 Eureka 整合 Ribbon

  1. Ribbon 应用于消费方(项目的 80 端口),因此直接在 80 端口模块中导入依赖
<!--Eureka依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
<!--ribbon依赖,需要两个都导入-->
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon</artifactId>
    <version>2.7.18</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>
  1. 然后就是编写配置文件
server:
  port: 80

# Eureka 配置
eureka:
  client:
    service-url: 
      defaultZone: http://www.jiuxiao.com:7001/eureka/,http://www.huang.com:7002/eureka/,http://www.wudao.com:7003/eureka/
  1. 然后在启动类上使用注解开启 Eureka
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer80 {

    public static void main(String[] args) {
        SpringApplication.run(DeptConsumer80.class, args);
    }
}
  1. 当初在创建 80 端口的模块时,我们在 ConfigBean 配置类中注册了一个 RestTemplate 的 bean,现在需要让这个 bean 加上负载均衡的功能
@Configuration
public class ConfigBean {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 在 80 端口的 Controller 中,我们设定的访问地址是固定的,即 http://localhost:8001,但是在正式环境中,应该是使用 ApplicationName 去获取该应用对应的地址

image-20220628211501346

因为现在我们只有一个服务提供方 8001,所以直接修改为 8001 的 ApplicationName,后续我们新增服务后再改变

private static final String REST_URL_PREFIX = "http://SPRING-CLOUD-PROVIDER-DEPT";
  1. 启动项目三个集群7001、7002、7003、提供者 8001、消费者 80,访问 Eureka 监控页面,8001 和 80 均被监测到(消费者我们没有设置 ApplicationName,所以显示的是 unknown)

image-20220628212345886

  1. 现在我们访问 80 端口的请求,比如根据 ID 查询部门

image-20220628213633671

Eureka 现在是通过负载均衡的算法,随机使用 7001、7002、7003 其中之一进行的请求访问

可能这里感受不到,这是因为我们现在只有一个提供者 8001

Ribbon 负载均衡的算法无论是什么,计算结果只能为 db01,访问的都是固定的那个 db01 数据库,所以我们需要多创建几个提供者

5.4 增加提供者

image-20220629092726265

  1. 这里一个服务提供者对应一个数据库,因此要再创建和 db01 类似的两个数据库 db02、db03
-- 创建 db02 数据库
create database `db02`;

use `db02`;

create table `dept`(
    `dept_id` int(32) not null primary key auto_increment comment '部门编号',
    `dept_name` varchar(150) not null comment '部门名称',
    `db_source` varchar(150) not null comment '该资源所存储的数据库'
) comment '部门表';

insert into db02.dept (dept_name, db_source) value ('开发部', database());
insert into db02.dept (dept_name, db_source) value ('人事部', database());
insert into db02.dept (dept_name, db_source) value ('财务部', database());
insert into db02.dept (dept_name, db_source) value ('市场部', database());
insert into db02.dept (dept_name, db_source) value ('运维部', database());


-- 创建 db03 数据库
create database `db03`;

use `db03`;

create table `dept`(
    `dept_id` int(32) not null primary key auto_increment comment '部门编号',
    `dept_name` varchar(150) not null comment '部门名称',
    `db_source` varchar(150) not null comment '该资源所存储的数据库'
) comment '部门表';

insert into db03.dept (dept_name, db_source) value ('开发部', database());
insert into db03.dept (dept_name, db_source) value ('人事部', database());
insert into db03.dept (dept_name, db_source) value ('财务部', database());
insert into db03.dept (dept_name, db_source) value ('市场部', database());
insert into db03.dept (dept_name, db_source) value ('运维部', database());

这三个数据库除过数据库名称字段不一致外,其他均一致

image-20220629094304158

image-20220629094242615

image-20220629094320358

  1. 然后就是创建另外两个提供者,端口号分别设置为 8002、8003,与 8001 配置基本一致(记得修改Mapper中的数据库,不然一致都是 db01)

  2. 现在我们启动三个集群(7001、7002、7003),然后启三个服务(8001、8002、8003),打开 Eureka 监控页面,发现三个服务均已经被注册

image-20220629100004008

  1. 最后启动 80 消费者模块,访问 http://localhost/consumer/dpet/list 请求,查询部门列表

image-20220629100558116

可以看到,第一次请求的数据是从数据库 db01 中查询的,我们再次访问该请求,数据库变成了 db02

image-20220629102739833

不断访问,发现查询的数据是 db01、db02、db03 三个数据库轮流查询,这就是负载均衡的轮询算法

image-20220629102752497

5.5 自定义负载均衡算法

现在我们不想使用默认的轮询算法,想自定义一个算法去实现负载均衡,怎么操作?

全局搜索一个名为 IRule 的接口,然后看他的实现类,这些实现类就是 Ribbon 的负载均衡算法

image-20220629153909278

并且,Ribbon 所有的负载均衡算法,均继承了一个名为 AbstractLoadBalancerRule 的父类

那么由此可知,我们要自定义负载均衡算法,同样只需要继承 AbstractLoadBalancerRule 父类,然后重写父类方法即可

image-20220629152946883

需要注意的是,我们自定义 Ribbon 配置类,不能和主启动类同级,这在官网有明确描述

image-20220630092728594

因此要如下所示建包

image-20220630092523968

假设我们自定义的算法为:每个节点访问 3 次之后,换下一个节点,如此反复

public class JXRandomRule extends AbstractLoadBalancerRule {

    //当前服务第几次被调用
    private int total = 0;
    //当前是第几个服务在提供服务
    private int currentIndex = 0;

    @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }

        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }

            //获得活着的服务
            List<Server> upList = lb.getReachableServers();
            //获得所有服务
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                return null;
            }

            if (total < 3){
                server = upList.get(currentIndex);
                total++;
            }else{
                total = 0;
                currentIndex++;
                if (currentIndex > upList.size()){
                    currentIndex = 0;
                }
                server = upList.get(currentIndex);
            }

            if (server == null) {
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            server = null;
            Thread.yield();
        }

        return server;

    }

    protected int chooseRandomInt(int serverCount) {
        return ThreadLocalRandom.current().nextInt(serverCount);
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}

然后在 MyRule 类中调用该算法

@Configuration
public class JXRule {

    @Bean
    public IRule myBalancerRule() {
        return new JXRandomRule();
    }
}

在启动类上需要使用注解指定我们的自定义配置类

@SpringBootApplication
@EnableEurekaClient
//在微服务启动的时候,就可以加载我们自己配置的Ribbon类
@RibbonClient(name = "SPRING-CLOUD-PROVIDER-DEPT", configuration = JXRule.class)
public class DeptConsumer80 {
    //TODO: 80
    public static void main(String[] args) {
        SpringApplication.run(DeptConsumer80.class, args);
    }
}
posted @   悟道九霄  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示