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 简介
2.1 什么是 SpringCloud
-
SpringCloud 为开发者提供了一套微服务解决方案,包括服务注册与发现、配置中心、全链路监控、服务网关、负载均衡、熔断器等组件
-
SpringCloud 巧妙的简化了分布式系统基础设施的开发,为开发人员提供了 配置管理、服务发现、断路器、路由、微代理、时间总线、全局锁、决策竞选、分布式会话 一些列快速构建分布式系统的工具
-
SpringCloud 是分布式微服务架构之下的一站式解决方案,是各个微服务架构落地技术的集合体
2.2 与 SpringBoot 的关系
-
SpringBoot 专注于快速方便地开发单个个体微服务
-
SpringCloud 是专注于全局的微服务之间的协调和治理,为各个微服务模块之间提供集成服务
-
SpringBoot 可以离开 SpringCloud 独立开发项目,但是 SpringCloud 却离不开 SpringBoot,它依赖于 SpringBoot 进行开发
-
SpringBoot 专注于快速、方便地开发 单体微服务,SpringCloud 专注于 全局的微服务治理
2.3 分布式 + 服务治理
目前成熟的大型网站,互联网架构一般为:应用服务化拆分 + 消息中间件
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 模块
-
创建第一个子项目
spring-cloud-api
,只导入 lombok 依赖 -
新建一个数据库,名为
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());
- 创建于数据库表对应的实体类
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 模块
- 创建新的子模块(端口号: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>
- 接下来去写该模块的配置文件
# 服务配置
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
- 编写 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();
}
- 编写 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>
- 然后是对应的 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();
}
}
- 写一个主启动类
/**
* 启动类
* @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 环境搭建:服务消费者
- 创建一个服务消费者子项目(端口号: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>
- 编写配置文件
server:
port: 80
- 该模块作为消费者,是没有 Service 层的,那么在 Controller 中怎么获得要请求的 url ?
我们想到了 SpringBoot 中一个特殊的东西:模板(xxxTemplate
)
考虑到 SpringBoot 中很多都是 Restful 风格的请求,所以我们全局搜索 RestTemplate
在该类中,查看所有方法,我们发现,他有一些方法的开头是 get、post、put、delete
,这不正对应着请求的方式?
不出意外的话,就要使用这些方法获取请求的 url 了,那么首先,我们应该将它注册为 bean
- 新建一个配置类,将 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();
}
}
- 在 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);
}
}
- 启动项目,现在生产者端进行访问(8001端口)
然后再去消费者端,模拟用户进行访问(80端口)
完全不同的 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 Server 、Eureka Client
-
Eureka Server 提供服务注册服务,各个节点启动之后,会在 Eureka Server 中进行注册,服务注册表中会存储所有可用的服务结点信息
-
Eureka Client 是一个 Java 客户端,用于简化 Eureka Server 的交互,客户端同时也具备一个内置的、使用轮询负载算法的负载均衡器
-
Eureka Client 会在应用启动之后,,每隔一段时间(默认 30 秒)会像 Eureka Server 发送心跳,若有回复则表示依旧在保持连接;否则 Eureka Server 会将该服务节点从服务注册表中移除掉(默认周期为 90 秒)
4.3 Eureka 的三大角色
-
Eureka Server:提供服务的注册与发现(类似于 Zookeeper)
-
Eureka Provider:将自身的服务注册到 Eureka 中,从而使得消费方能够找到
-
Eureka Consumer:服务消费方从 Eureka 中获取注册服务列表,从而找到消费服务
4.4 EurekaServer 搭建
- 依旧在上述的父项目中新建子项目(端口号: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>
- 编写配置文件
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/
- 启动类上加上
@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);
}
}
- 启动项目,访问我,我们配置的 7001 端口,Eureka 环境搭建成功
4.5 注册 Eureka 服务
上面已经写好了 Eureka 服务,现在需要将 Eureka 服务注册到服务提供者中(端口:8001)
- 首先在服务提供者(端口: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>
- 然后去 yml 中配置 Eureka
# Eureka 配置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/
- 最后就是去启动类上使用注解开启 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);
}
}
- 依次启动 7001 的 Eureka Server、 8001 的 Eureka Client 服务,两个服务启动之后,访问 4.4 中 Eureka 的监控页面,可以发现已经监控到了 8001 端口的服务
这里的 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
4.6.2 自我保护机制
我们将两个端口的服务均启动之后,将 8001 端口的服务手动结束,模拟网络故障,这个时候再去 Eureka 监控面板
8001 的服务依旧被保留,面板上方出现了红色的提示信息(紧急情况! EUREKA 可能不正确地声称实例已启动,但实际上并未启动。 续订少于阈值,因此为了安全起见,实例不会过期)
-
某一时刻一个微服务不可用了,Eureka 并不会立刻清理,依旧会对该服务进行信息保存
-
默认情况下,若 Eureka 在 90 秒内没有接收到某个微服务实例的心跳,Eureka Server 将会对该实例进行注销。但是,当网络故障等不可控情况发生时,Eureka 无法与微服务实例之间实现通信,但是并不是该服务实例的问题,这个时候 Eureka Server 就会进入自我保护模式,在该模式中,不再会自动删除注册表中的数据(不会销毁任何实例),当网络故障恢复之后,该 Eureka Server 结点就会自动退出自我保护模式,恢复正常
-
该机制的思想就是:宁可保留不健康的微服务,也不会盲目的注销任何一个健康的微服务
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,前端输出了该微服务的相关信息,控制台同时也清晰打印了出来
4.7 集群环境配置
-
现在搭建一个小型集群,建立两个新的模块,端口号分别为 7002、7003,配置文件(只需修改端口号)以及依赖均和 7001 一致
-
前边在 Eureka 配置中,只关联了一个结点,但是要做集群,每个结点都要映射另外几个,这应该怎么做?很简单,在配置中直接将另外几个直接挂载
-
首先要去计算机的 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/
- 现在 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/
- 首先依次启动三个集群,最后再启动提供者 8001 端口,当我们访问 7001 对应的监控页面时,已经出现了另外两个集群,并且监控着 8001 实例
- 此时,我们将 7002 端口手动结束来模拟不可抗意外导致的集群错误,访问 7001 或者 7003,可以看到,服务仍然正常
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 原则
-
向注册中心查询服务列表时,我们可以忍受注册中心返回的是几分钟之前的注册信息,但不能接受服务直接 down 掉不可用
-
服务注册的可用性要求高于一致性
-
master 结点因为网络故障与其他结点失去联系时,剩余结点会重新进行 leader 选举
-
但是时间过长(30s ~ 120s),并且选举期间整个 zookeeper 集群是不可用的,这就导致在选举期间注册服务瘫痪
-
虽然服务最终能恢复,但是漫长的选举时间导致注册长时间不可用这是不能容忍的(可用性 ↓)
Eureka 保证的是 AP 原则
-
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
- 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>
- 然后就是编写配置文件
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/
- 然后在启动类上使用注解开启 Eureka
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer80 {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer80.class, args);
}
}
- 当初在创建 80 端口的模块时,我们在 ConfigBean 配置类中注册了一个 RestTemplate 的 bean,现在需要让这个 bean 加上负载均衡的功能
@Configuration
public class ConfigBean {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 在 80 端口的 Controller 中,我们设定的访问地址是固定的,即 http://localhost:8001,但是在正式环境中,应该是使用 ApplicationName 去获取该应用对应的地址
因为现在我们只有一个服务提供方 8001,所以直接修改为 8001 的 ApplicationName,后续我们新增服务后再改变
private static final String REST_URL_PREFIX = "http://SPRING-CLOUD-PROVIDER-DEPT";
- 启动项目三个集群7001、7002、7003、提供者 8001、消费者 80,访问 Eureka 监控页面,8001 和 80 均被监测到(消费者我们没有设置 ApplicationName,所以显示的是 unknown)
- 现在我们访问 80 端口的请求,比如根据 ID 查询部门
Eureka 现在是通过负载均衡的算法,随机使用 7001、7002、7003 其中之一进行的请求访问
可能这里感受不到,这是因为我们现在只有一个提供者 8001
Ribbon 负载均衡的算法无论是什么,计算结果只能为 db01,访问的都是固定的那个 db01 数据库,所以我们需要多创建几个提供者
5.4 增加提供者
- 这里一个服务提供者对应一个数据库,因此要再创建和 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());
这三个数据库除过数据库名称字段不一致外,其他均一致
-
然后就是创建另外两个提供者,端口号分别设置为 8002、8003,与 8001 配置基本一致(记得修改Mapper中的数据库,不然一致都是 db01)
-
现在我们启动三个集群(7001、7002、7003),然后启三个服务(8001、8002、8003),打开 Eureka 监控页面,发现三个服务均已经被注册
- 最后启动 80 消费者模块,访问
http://localhost/consumer/dpet/list
请求,查询部门列表
可以看到,第一次请求的数据是从数据库 db01 中查询的,我们再次访问该请求,数据库变成了 db02
不断访问,发现查询的数据是 db01、db02、db03
三个数据库轮流查询,这就是负载均衡的轮询算法
5.5 自定义负载均衡算法
现在我们不想使用默认的轮询算法,想自定义一个算法去实现负载均衡,怎么操作?
全局搜索一个名为 IRule
的接口,然后看他的实现类,这些实现类就是 Ribbon 的负载均衡算法
并且,Ribbon 所有的负载均衡算法,均继承了一个名为 AbstractLoadBalancerRule
的父类
那么由此可知,我们要自定义负载均衡算法,同样只需要继承 AbstractLoadBalancerRule
父类,然后重写父类方法即可
需要注意的是,我们自定义 Ribbon 配置类,不能和主启动类同级,这在官网有明确描述
因此要如下所示建包
假设我们自定义的算法为:每个节点访问 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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)