![image-20220326101610252](SpringCloud
文章目录
- ![image-20220326101610252](SpringCloud
- @[toc] 一、什么是微服务架构
-
- 二、版本选型
-
- 三、Cloud组件停更/升级/替换
-
- 四、微服务架构编码构建
-
- 五、Eureka服务注册与发现
-
- 六、Zookeeper服务注册与发现
-
- 七、Consul服务注册与发现
-
- 八、Ribbon负载均衡服务调用
-
- 九、OpenFeign服务接口调用
-
一、什么是微服务架构
文章目录
- ![image-20220326101610252](SpringCloud
- @[toc] 一、什么是微服务架构
- 二、版本选型
- 三、Cloud组件停更/升级/替换
- 四、微服务架构编码构建
- 五、Eureka服务注册与发现
- 六、Zookeeper服务注册与发现
- 七、Consul服务注册与发现
- 八、Ribbon负载均衡服务调用
- 九、OpenFeign服务接口调用
由马丁弗勒提出的一种架构模式
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于Http协议的Restful API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等,另外,应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建
1.1 SpringCloud官网
https://spring.io/projects/spring-cloud
Spring Cloud 为开发者提供了工具来快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话,集群状态)。分布式系统的协调导致了样板模式,使用 Spring Cloud 开发人员可以快速建立实现这些模式的服务和应用程序。它们在任何分布式环境中都能很好地工作,包括开发人员自己的笔记本电脑、裸机数据中心以及 Cloud Foundry 等托管平台。
特征
Spring Cloud 专注于为典型用例提供良好的开箱即用体验,并提供可扩展机制以覆盖其他用例。
- 分布式/版本化配置
- 服务注册和发现
- 路由
- 服务到服务调用
- 负载均衡
- 断路器
- 全局锁
- 领导选举和集群状态
- 分布式消息传递
1.2 国内网站服务架构
1.3 SpringCloud主流技术栈
二、版本选型
- Cloud H版
- Boot 2.2.x
2.1 官网中Cloud与boot版本的对应关系
2.2 本次学习工具的版本号
技术 | 版本号 |
---|---|
SpringCloud | Hoxton.SR1 |
SpringBoot | 2.2.2 RELEASE |
SpringCloud alibaba | 2.1.0 RELEASE |
Java | java8 |
Maven | 3.5及以上 |
MySQL | 5.7及以上 |
三、Cloud组件停更/升级/替换
3.1 以前
3.2 现在
- SpringCloud
- 官网:https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle
- 中文文档:https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md
- SpringBood
- 官网:https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/
四、微服务架构编码构建
4.1 开发规定
约定 > 配置 > 编码
4.2 IDEA新建project工作空间
4.2.1 聚合父工程Project
步骤:
1.New Project
2.聚合总父工程名字
3.Maven选版本
4.工程名字
5.字符编码
6.注解生效激活
7.java编译版本选8
8.File Type过滤
-
New Project
-
聚合总父工程名字
-
Maven选版本
-
工程名字
-
字符编码
-
注解生效激活
-
java编译版本选8
-
File Type过滤
4.2.2 父工程POM
-
将打包方式设为packing
-
删掉父工程idea,src文件夹,只留下pom文件,干净整洁
-
统一管理jar包版本
POM文件
<?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.lebrwcd</groupId>
<artifactId>SpringCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 统一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
注意:父工程pom文件中${*.version}全部爆红是不用管它的,也不用换版本。原因是maven本地仓库里没有这个版本的。在子pom中实际引入依赖后,M会自动下载
4.2.3 DependencyManagement 与 Dependencies
4.2.3.1 DependencyManagement
Maven 使用dependencyManagement 元素来提供了一种管理依赖版本号的方式
通常会在一个组织或者项目的最顶层的父POM 中看到dependencyManagement 元素。
使用pom.xml 中的dependencyManagement 元素能让所有子项目引用一个依赖而不用显式的列出版本号。
Maven 会沿着父子层次向上走,直到找到一个拥有dependencyManagement 元素的项目,然后它就会使用这个 dependencyManagement
元素中指定的版本号。
这样做的好处就是:如果有多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号,这样当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要一个一个子项目的修改 ;另外如果某个子项目需要另外的一个版本,只需要独立声明version即可。
-
dependencyManagement里只是声明依赖,并不实现引入依赖,因此子项目需要显示的声明需要用的依赖。
-
如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,
才会从父项目中继承该项,并且version和scope都读取自父pom;
- 如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。
4.2.3.2 Maven中跳过单元测试
4.2.4 mvn:install将父pom存入仓库以便子pom继承
4.3 Rest微服务工程构建
4.3.1 构建步骤
4.3.1.1 cloud-provider-payment8001
微服务提供者支付Module模块
步骤:
1. 建module
2. 改pom
3. 写yml
4. 主启动
5. 业务类
5.1 建表sql
5.2 entities
5.3 dao/mapper
5.4 service
5.5 controller
6. 测试
-
建module
查看父工程pom文件变化:
<groupId>com.lebrwcd</groupId> <artifactId>SpringCloud</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>cloud-provider-payment8001</module> </modules>
-
改pom
<?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"> <!--父工程--> <parent> <artifactId>SpringCloud</artifactId> <groupId>com.lebrwcd</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <!--gav坐标不用写g,v,用父类的--> <artifactId>cloud-provider-payment8001</artifactId> <!--父工程只是声明依赖,子工程实际引入依赖,版本号不用写,父工程统一管理--> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--mysql-connector-java--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
写yml
server: port: 8001 spring: application: name: cloud-provider-payment8001 datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud_db?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: wcd0209 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lebrwcd.springcloud.entities # 所有Entity别名类所在包
-
主启动
@SpringBootApplication public class paymentMain { public static void main(String[] args) { SpringApplication.run(paymentMain.class,args); } }
-
业务类
-
建表SQL
CREATE TABLE `payment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `serial` varchar(200) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
-
entities
-
主实体Payment
@Data @AllArgsConstructor @NoArgsConstructor public class Payment implements Serializable { private Long id; private String serial; }
-
Json封装体CommonResult
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public CommonResult(Integer code,String message){ this(code,message,null); } }
-
-
dao
-
dao接口
@Mapper //mapper注解 public interface PaymentDao { public int create(Payment payment); public Payment getPaymentById(@Param("id") Long id); }
-
dao接口映射文件mapper.xml src/main/resources/mapper/PaymentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.lebrwcd.springcloud.dao.PaymentDao"> <!--映射的实体map--> <resultMap id="PaymentMap" type="com.lebrwcd.springcloud.entities.Payment"> <id column="id" property="id" jdbcType="BIGINT"/> <id column="serial" property="serial" jdbcType="VARCHAR"/> </resultMap> <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id"> insert into payment(serial) values (#{serial}) </insert> <select id="getPaymentById" resultMap="PaymentMap"> select * from payment where id = #{id} </select> </mapper>
-
-
service
-
service接口
public interface PaymentService { int create(Payment payment); Payment getPaymentById(@Param("id") Long id); }
-
service实现类
@Service //service注解 public class PaymentServiceimpl implements PaymentService { @Autowired private PaymentDao paymentDao; @Override public int create(Payment payment) { return paymentDao.create(payment); } @Override public Payment getPaymentById(Long id) { return paymentDao.getPaymentById(id); } }
-
-
controller
- Restful风格
@RestController @Slf4j public class PaymentController { @Autowired private PaymentService paymentService; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment){ int result = paymentService.create(payment); log.info("插入数据返回结果:" + result); if(result > 0){ return new CommonResult(200,"插入成功",payment); }else{ return new CommonResult(404,"插入失败",null); } } @GetMapping(value = "/payment/get/{id}") public CommonResult get(@PathVariable("id") Long id){ Payment payment = paymentService.getPaymentById(id); log.info("插入数据:{}",id); if(payment != null){ return new CommonResult(200,"查询成功",payment); }else{ return new CommonResult(404,"查询失败,所查id为:"+id,null); } } }
-
-
测试
- http://localhost:8001/payment/get/1
post请求的去postman测试
小总结
1. 建module
2. 改pom
3. 写yml
4. 主启动
5. 业务类
5.1 建表sql
5.2 entities
5.3 dao/mapper
5.4 service
5.5 controller
6. 测试
get-->浏览器地址栏
其余请求-->postman
4.3.1.2 热部署
-
Adding devtools to your project
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
-
Addinf plugin to your pom.xml
下段配置我们粘贴进聚合父类总工程的pom.xml里
<build> <finalName>cloud-provider-payment8001</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>
-
Enabling automatic build
-
Update the value of
-
重启IDEA
4.3.1.3 cloud-consuemer-order80
微服务消费者订单Module模块
同样的步骤:
-
建module
-
改pom
-
写yaml
-
这里我用的是81端口,80端口被占用了
server: port: 81
-
-
主启动
@SpringBootApplication public class OrderMain81(){ public static void main(String[] args){ SpringApplication.run(OrderMain81.class,args); } }
-
业务类
-
entities:Payment、CommonResult
-
RestTemplate
-
RestTemplate
是执行HTTP请求的同步阻塞式的客户端,它在HTTP客户端库(例如JDK HttpURLConnection,Apache HttpComponents,okHttp等)基础封装了更加简单易用的模板方法API。也就是说RestTemplate是一个封装,底层的实现还是java应用开发中常用的一些HTTP客户端。但是相对于直接使用底层的HTTP客户端库,它的操作更加方便、快捷,能很大程度上提升我们的开发效率。RestTemplate
作为spring-web项目的一部分,在Spring 3.0版本开始被引入。RestTemplate类通过为HTTP方法(例如GET,POST,PUT,DELETE等)提供重载的方法,提供了一种非常方便的方法访问基于HTTP的Web服务。如果你的Web服务API基于标准的RESTful风格设计,使用效果将更加的完美。 -
Spring RestTemplate
是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集 -
如何使用?
使用restTemplate访问restful接口非常的简单粗暴无脑。
(url, requestMap, ResponseBean.class)这三个参数分别代表
REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
- 客户端向服务端提交的是post请求
-
客户端向服务端提交的是get请求
-
-
config配置类
-
ApplicationContextConfig
@Configuration public class ApplicationContextConfig { //往容器中注入RestTemplate组件 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
-
-
controller
客户端只需要写controller,因为客户端(消费者)不可能能连接到后台服务的dao,service
@RestController public class OrderController { public static final String SERV_URL = "http://localhost:8001" ; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/payment/create") public CommonResult<Payment> create(Payment payment){ //第一个参数是Rest请求的url,第二个参数是请求参数,第三个参数是响应类型 return restTemplate.postForObject(SERV_URL+"/payment/create",payment,CommonResult.class); } @GetMapping("/consumer/payment/get/{id}") public CommonResult<Payment> getPayment(@PathVariable("id") Long id){ //第一个参数是Rest请求的url,第二个参数是响应类型,第三个参数是url路径变量 return restTemplate.getForObject(SERV_URL+"/payment/get/"+id,CommonResult.class,id); } }
-
-
测试
null的原因是服务端的controller中的create方法中,请求参数没有用@RequestBody接收,加上@RequestBody就好了
IDEA2021版的RunDashBoard就是Service
4.3.1.4 工程重构
- 我们发现这两个服务之间有重复的内容,我们需要把重复的提取出来,打成jar包供两者使用
重构步骤:
-
新建module:cloud-api-common
-
写pom
<?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"> <parent> <artifactId>SpringCloud</artifactId> <groupId>com.lebrwcd</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-api-common</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.1.0</version> </dependency> </dependencies> </project>
-
提取公用的entities
-
在cloud-api-common中新建entities
-
安装到本地maven仓库中,maven中,依此点击clean、install
-
删掉两个服务中entities包,在它们的Pom文件中加入:
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <groupId>com.lebrwcd</groupId> <artifactId>cloud-api-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
-
重构完成
4.3.2 目前工程样图
我们发现,自己这样一个提供者,一个消费者也可以运行,但是如果以后消费者多了,服务者是否有空闲能够对消费者进行服务?比如医院,假设一开始是一个病人一个医生,那可以很好的进行服务,但是如果病人多了,医生忙不过来,是否需要加上一种门诊,也就是挂号服务,还有多少流量,今天医生还能接多少病人啊等等情况,所以就出现了服务注册中心,服务提供者往服务注册中心注册自己的服务,下一节开始讲解服务注册中心
五、Eureka服务注册与发现
<
5.1 基础知识
5.1.1 什么是服务治理
Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
5.1.2 什么是服务注册与发现
Eureka采用了CS的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息 比如 服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用远程RPC调用框架,核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
5.1.3 Eureka两组件
Eureka包含两个组件:Eureka Server和Eureka Client
5.1.3.1 Eureka Server
Eureka Server提供服务注册服务
@EnableEurekaServer
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
5.1.3.2 Eureka Client
@EnableEurekaClient
EurekaClient通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
5.2 单点Eureka
单点Eureka指的是只有一台服务注册中心
5.2.1 Eureka-Server服务端 7001
服务注册中心,类似于物业公司
步骤:
-
建module : cloud-eureka-server7001
-
改pom
<?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"> <parent> <artifactId>SpringCloud</artifactId> <groupId>com.lebrwcd</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-eureka-server7001</artifactId> <dependencies> <!--eureka-server--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>com.lebrwcd</groupId> <artifactId>cloud-api-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--boot web actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--一般通用配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies> </project>
-
写yaml
server: port: 7001 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
- 主启动
@EnableEurekaServer
@SpringBootApplication @EnableEurekaServer //声明自己是Eureka服务注册中心 public class EurekaMain7001 { public static void main(String[] args) { SpringApplication.run(EurekaMain7001.class,args); } }
- 主启动
-
测试 地址栏上输入 :http://localhost:7001
5.2.2 Eureka-Client客户端 8001
服务提供者,需要先入驻物业公司(注册),才能对外提供服务
步骤:
-
改module,改之前的cloud-provicder-payment8001
-
改pom
在原先的pom文件基础上加上:
<!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
写yaml
server: port: 8001 spring: application: name: cloud-provider-payment8001 #微服务名称 datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud_db?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: wcd0209 #eureka配置 eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lebrwcd.springcloud.entities # 所有Entity别名类所在包
-
主启动
在主启动类上添加注解:
EnableEurekaClient
,表示自己将向注册中心Server注册,对外提供服务 -
测试:
-
需要先启动EurekaServer7001
-
访问:http://localhost:7001/
-
spring:
application:
name: cloud-provider-payment8001 #微服务名称 这里的名称就是Eureka中注册进去的服务名称 -
自我保护机制
-
5.2.3 Eureka-Client 81
服务消费者,可以选择在注册中心注册,也可选择不注册,服务消费者与服务注册中心进行单点连接,获得服务注册中心中的服务,然后通过远程过程调用的方式获得服务提供者中对应的服务
步骤:
-
改module cloud-consumer-order81
-
改pom,在原先pom的基础上加上:
<!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
写yaml
server: port: 81 spring: application: name: cloud-order-service #微服务名称 eureka: client: fetch-registry: true #抓取已有的注册信息 #不注册到eureka服务注册中心中 register-with-eureka: false service-url: defaultZone: http://localhost:7001/eureka
-
主启动,在主启动类上加入
@EnableEurekaClient
-
测试
-
不注册到注册中心的情况
-
抓取注册中心中的服务,cloud-provider-payment8001,通过RPC远程过程调用
-
5.3 Eureka集群
5.3.1 集群原理说明
Eureka集群的原理:互相注册,相互守望
5.3.2 Eureka集群环境构建步骤
步骤:
-
参考7001新建module7002
-
改7002的pom
-
修改host映射配置
- 找到C:\windows32\drivers\etc\中的host文件
- 修改映射配置进host文件
- 127.0.0.1 eureka7001.com
- 127.0.0.1 eureka7002.com
-
写yaml
以前的单机配置:
server: port: 7001 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: #false表示不向注册中心注册自己。 register-with-eureka: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 fetch-registry: false service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
现在的集群配置:
-
7001:
server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://eureka7002.com:7002/eureka/ #互相注册,相互守望
-
7002:
server: port: 7002 eureka: instance: hostname: eureka7002.com #eureka服务端的实例名称 client: register-with-eureka: false #服务注册中心不向自己注册 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7001.com:7001/eureka/
注意:出现如下错误是因为yaml文件格式错误,tab,空格问题
-
-
主启动,在7002上面加上
@EnableEurekaServer
-
测试:
-
地址栏:http://eureka7001.com:7001
-
地址栏:http://eureka7002.com:7002
“互相注册,相互守望”
-
5.3.3 将支付服务8001与订单服务81发布到上面两台Eureka集群
- 修改两个模块中的Yaml
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版配置
-
测试:
- 先启动服务注册中心,EurekaServer,7001/7002
- 再启动服务提供者provider,8001
- 再启动服务消费者consumer,81
5.3.4 服务提供者集群配置
步骤:
-
新建module:cloud-provider-payment8002
-
拷贝8001的内容,除了主启动
-
修改yaml,复杂8001的,修改端口号为8002即可
server: port: 8002 spring: application: name: cloud-payment-Service #微服务名称 datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud_db?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: wcd0209 eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: #defaultZone: http://localhost:7001/eureka #单机版 defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lebrwcd.springcloud.entities # 所有Entity别名类所在包
-
修改controller,为了后面负载均衡方便查看效果,8001/8002都改
-
public class PaymentController { @Value("${server.port}") private String serverPort; @Resource private PaymentService paymentService; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment) { int result = paymentService.create(payment); log.info("*****插入操作返回结果:" + result); if(result > 0) { return new CommonResult(200,"插入成功,返回结果"+result+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"插入失败",null); }
-
-
测试
-
先开启服务注册中心,7001/7002
-
再开启服务提供者:8001/8002
-
观看服务注册中心
-
服务提供者自测:
5.3.5 负载均衡
继续上面的步骤,再开启服务消费者:81
我们刷新,发现端口号一直是8001,我们不是弄了两个服务提供者吗,怎么一直是8001,问题是我们服务消费者order中把服务提供者中的地址给写死了,如果在单机环境下可以这么做,但是在服务提供者集群环境下,不能这么做
//public static final String SERV_URL = "http://localhost:8001" ; 服务提供者集群环境下地址不能写死
//通过在eureka上注册过的微服务名称调用
public static final String SERV_URL = "http://CLOUD-PAYMENT-SERVICE" ;
好,我们再来测试一下:http://localhost:81/consumer/payment/get/5
- 这个需要使用
@LoadBalanced
赋予RestTemplate负载均衡的能力,默认采用轮询的方式
结果:
刷新一下:
类似于Nginx中的负载均衡默认轮询方式,即请求过来了,会平均分配给Nginx子进程
5.3.6 小总结
5.3.6.1 单机Eureka
一台服务注册中心,一个服务提供者,一个服务消费者
5.3.6.2 服务注册中心集群Eureka
多台服务注册中心,一台服务提供者,一个服务消费者
5.3.6.3 Eureka常规部署
多台服务注册中心,多台服务提供者,一个服务消费者
- 服务注册中心之间互相注册,相互守望
- 服务提供者如果微服务名一样,则处于一个微服务下,以端口号区分
- 服务消费者与服务提供者通信时RestTemplate记得开启负载均衡
Eureka yaml配置
#服务注册中心:7001为例
#以eureka开始
eureka:
#实例
instance:
hostname: eureka7001.com #eureka服务端的实例名称
#往服务注册中心注册的对象
client:
register-with-eureka: false #false表示不向注册中心注册自己。一般是服务注册中心这么配置
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://eureka7002.com:7002/eureka/ #互相注册,相互守望
#服务提供者:
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
#服务消费者:
eureka:
client:
#服务消费者一定要拉取到服务注册中心中的注册信息,即有哪些服务
fetch-registry: true
#是否注册到服务注册中心,可false,可true
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
5.4 actuator微服务信息完善
5.4.1 主机名称修改
5.4.1.1 修改服务提供者的yaml文件
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: payment8001
prefer-ip-address: true
5.4.1.2 效果
5.4.2 ip地址提示
5.4.2.1 修改yaml
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: payment8001 #实例id,展示在web页面
prefer-ip-address: true #显示ip地址
5.4.2.2 效果
5.5 服务发现Discovery
对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
5.5.1 对8001进行自测
-
修改controller
@Resource private DiscoveryClient discoveryClient; @GetMapping("/payment/discovery") public Object discovert(){ //获得该服务注册中心上有哪些微服务 List<String> services = discoveryClient.getServices(); for (String service : services) { log.info("*****service:" + service); } //通过微服务名称获得其对应的信息 List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); for (ServiceInstance instance : instances) { log.info(instance.getServiceId() + "**" + instance.getHost()+" "+instance.getPort()+" "+ instance.getUri()); } return discoveryClient; }
-
修改主程序:加上注解
@EnableDiscoveryClient
@SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class,args); } }
-
测试:
5.6 Eureka自我保护
5.6.1 概述
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,
Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE
紧急!Eureka可能错误地声称实例已经启动,而实际上它们并没有。更新小于阈值,因此为了安全实例不会过期
5.6.2 为什么会产生Eureka自我保护机制?
为了防止EurekaClient可以正常运行,但是 与 EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
相当于你欠物业中心2000快,但是物业中心不会这么快把你从注册表中删除,而是会保留你的信息,直至你交钱了
5.6.2.1 什么是自我保护模式?
属于CAP分支里面的AP
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
`在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
5.6.2.2 测试
采用单机的eureka测试,即EurekaServer为7001,EurekaClient为8001
如果我们开启自我保护模式的话,假设我们8001出故障了,我们看看7001会不会马上把8001删除,如果开启保护模式的话是不会删除的
我们发现:7001还有保存8001
5.6.3 关闭自我保护机制
默认情况下自我保护机制是开启的,如何关闭自我保护机制呢?
步骤:
-
去7001修改yaml
eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://eureka7002.com:7002/eureka/ server: #关闭自我保护机制,保证不可用服务被及时踢除 enable-self-preservation: false eviction-interval-timer-in-ms: 2000
效果:
-
修改8001的yaml
eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka #单机版 #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版 instance: instance-id: payment8001 prefer-ip-address: true #心跳检测与续约时间 #开发时设置小些,保证服务关闭后注册中心能即使剔除服务 #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) lease-renewal-interval-in-seconds: 1 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务 lease-expiration-duration-in-seconds: 2
-
测试:先启动7001,再启动8001,关闭8001
关闭8001:假设8001出故障
超过2s后没有心跳直接将该服务剔除
Eureka停更了怎么办??
六、Zookeeper服务注册与发现
6.1 Zookeeper启动(linux)
zookeeper是一个分布式协调工具,可以实现注册中心功能
关闭linux服务器防火墙后启动zookeeper服务器,zookeeper服务器替代Eureka服务器,zk作为服务注册中心
-
启动Zk服务端(服务注册中心—》物业公司)
./zkServer.sh start
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1OylY3wK-1651920441550)(https://gitee.com/lebrwcd/typora/raw/master/img/image-20220413171531000.png)]
-
启动zk客户端,默认端口号2181
/zkCli.sh
一些基础环境要搭建好
- centos中zk的服务要开启,关闭防火墙不然windows端无法访问
- 启动zk客户端,要操控zk服务注册中心
- windows端cmd命令ping一下centos的ip
6.2 zk服务提供者
步骤:
-
创建module:cloud-provider-payment8004
-
写pom
<?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"> <parent> <artifactId>SpringCloud</artifactId> <groupId>com.lebrwcd</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-provider-payment8004</artifactId> <dependencies> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <groupId>com.lebrwcd</groupId> <artifactId>cloud-api-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- SpringBoot整合zookeeper客户端 --> <!--我这里服务器端的zk版本是3.7.0的,比较稳定,所以没有视频中出现的错误--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
写yaml
server: port: 8004 spring: application: name: cloud-payment-Service #微服务名称 cloud: #springcloud 整合 zookeeper zookeeper: connect-string: 192.168.110.131:2181 #zk服务注册中心
-
主启动
@SpringBootApplication @EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务 public class PaymentMain8004 { public static void main(String[] args) { SpringApplication.run(PaymentMain8004.class,args); } }
-
业务类
controller层
@RestController @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/zk") public Object zk(){ return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString(); } }
-
启动8004,测试
-
到centos的zk服务注册中心查看
6.2.1 zookeeper中的服务节点是临时节点
在Eureka中,Eureka自动开启保护机制,Eureka监听一个服务,当该服务没有心跳时,Eureka不会删除,而是保存着
与Eureka不同的是,Zk就可能比较强硬了,你没心跳了,就给我爬,直接把你给剔除,当你下一次恢复心跳了,对于zk来说,你已经不是上一次那个你了,你的流水号会改变
测试:当关闭8004,一段时间后查看zk状态
重启8004
6.3 zk服务消费者
步骤:
-
建module:cloud-consumerzk-order81
-
写pom
-
写yaml
-
主程序
-
业务类
@RestController @Slf4j public class OrderController { private static final String SERV_URL = "http://cloud-payment-Service"; @Autowired private RestTemplate restTemplate; @GetMapping("/consumer/payment/zk") public String invoke(){ return restTemplate.getForObject(SERV_URL+"/payment/zk",String.class); } }
-
测试:http://localhost:81/consumer/payment/zk
七、Consul服务注册与发现
7.1 Consul简介
官网:https://www.consul.io/
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。
7.2 功能
- 服务发现:提供HTTP和DNS两种发现方式
- 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本
- kv存储:Key、Value的存储方式
- 多数据中心:Consul支持多数据中心
- 可视化web界面
7.3 安装与允许
官网下载:https://www.consul.io/downloads
下载完成后解压,只有一个consul.exe文件
在该路径下进入cmd,开发者模式
查看版本号:consule --version
启动consul:consul agent -dev
<
访问web界面:`http://localhost:8500
7.4 服务提供者
步骤:
-
建module:cloud-provider-payment8006
-
改pom
<!--SpringCloud consul-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
-
写pom
server: port: 8006 spring: application: name: cloud-provider-service cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
-
主启动
@SpringBootApplication @EnableDiscoveryClient public class PaymentMain8006 { public static void main(String[] args) { SpringApplication.run(PaymentMain8006.class,args); } }
-
业务类
@RestController @Slf4j public class PaymentController { @Value("${server.port") private String serverPort; @GetMapping("/payment/consul") public String payment(){ return "springcloud with consul: "+serverPort+"\t\t"+ UUID.randomUUID().toString(); } }
-
测试:先启动8006,将服务注册到consul
-
http://localhost:8006/payment/consul
>
7.5 服务消费者
步骤和zookeeper的基本一致
就是pom改一下依赖,yaml改一下端口号,controller中的SERV_URL改一下,其余一样
测试:
先启动8006,再启动81
consul和zk一样,当服务提供者出现故障的时候,会剔除掉该服务
7.6 总结:三者异同
7.6.1 CAP
- C:Consistency(强一致性)
- A:Availability(可用性)
- P:Partition tolerance(分区容错性)
- CAP理论关注粒度是数据,而不是整体系统设计的策略
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
CAP理论的一个例子。开学了,你和同桌互对寒假作业答案,对到一半同桌去了厕所,恰好老师进来要收作业,
你决定:
AP【交就交吧,有错也无所谓了,交作业要紧】;
CP【等同桌回来,把答案对完再交,满分要紧】
八、Ribbon负载均衡服务调用
8.1 概述
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
官网:https://github.com/Netflix/ribbon/wiki
Ribbon目前也进入维护模式,未来的替换方案是:SpringCloud LoadBalance
但目前主流还是Spring Cloud Ribbon
8.2 LB(Load Balance)
LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。 Highly availability
常见的负载均衡有软件Nginx,LVS,硬件 F5等。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
-
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
-
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
8.2.1 集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发
至服务的提供方;
医院的大门,根据医院排班进入哪个科室
8.2.2 进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
医院科室,科室内有多个医生,询问有哪个医生是空闲的,选择合适的医生进行就诊
一句话:负载均衡 + RestTemplate调用
8.3 Ribbon负载均衡架构演示
Ribbon是站在服务消费者端的
Ribbon在工作时分成两步:
-
第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
-
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址(服务提供者)。
其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
总结:Ribbon就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中一个实例
为什么我们在前面Eureka的例子中能实现负载均衡呢?
-
ribbon依赖引入
<!--在引入EurekaClient的同时也引入了--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
-
RestTemplate:
@LoadBalance
原因是:我们在引入Eureka客户端的依赖时,它集成了Ribbon依赖
8.3.1 二说RestTemplate
官网:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
常用方法:
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);
<T> T postForObject(URI url, @Nullable Object request, Class<T> responseType);
<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);
<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);
<T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType);
8.3.1.1 getForObject & getForEntity
- getForObject返回的是json格式的字符串
- getForEntity返回的内容比较详细,返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
@GetMapping("/consumer/payment/get/{id}")
public CommonResult getPayment(@PathVariable("id") Long id){
log.info("查询Id:" + id);
return restTemplate.getForObject(SERV_URL+"/payment/get/"+id,CommonResult.class,id);
//{"code":200,"message":"查询成功,port:8002","data":{"id":5,"serial":"zpp001"}}
}
@GetMapping("/consumer/paymentEntity/get/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(SERV_URL + "/payment/get/" + id, CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();
//{"code":200,"message":"查询成功,port:8002","data":{"id":5,"serial":"zpp001"}}
}else{
return new CommonResult<>(444,"查询失败");
}
}
8.3.1.2 postForObject & postForEntity
-
postForObject返回的是json格式的字符串
-
postForEntity返回的内容比较详细,返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
8.4 Ribbon核心组件IRule
public interface IRule {
/*
* choose one alive server from lb.allServers or 通过特点算法中从服务列表中选取一个要访问的服务
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
IRule下的7中实现规则
默认采用轮询规则
8.4.1 如何替换规则
步骤:
-
自定义配置类
官方文档明确给出了警告: 这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下(即主启动类所在包及其子包) 否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
-
在主启动类上加
@RibbonClient
在启动该微服务的时候就能去加载我们的自定义Ribbon配置类,从而使配置生效
@SpringBootApplication @EnableEurekaClient @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)
@RibbonClient属性
- name/value:唯一标识一组客户端资源(服务提供者名称),包括负载平衡器。
- configuration:自定义规则的类
-
测试:http://localhost:81/consumer/payment/get/1
出现端口号随机的方式
8.5 Ribbon负载均衡算法
8.5.1 原理
算法:第n次请求数 % 集群数量 = 实际调用服务器位置下标
8.5.2 RoundRibonRule源码
public Server choose(ILoadBalancer lb, Object key) {
//如果没有设置负载均衡
if (lb == null) {
log.warn("no load balancer");
return null;
}
//起始默认服务数为0
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
//从负载均衡器中获取可达的服务,即活着的服务
List<Server> reachableServers = lb.getReachableServers();
//从负载均衡器获取所有服务
List<Server> allServers = lb.getAllServers();
//活着的服务的数量,Eureka-web界面中的Up(2),即有两台,8001,8002
int upCount = reachableServers.size();
//集群数量 8001,8002 两台
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//通过increateAndGetModulo
int nextServerIndex = incrementAndGetModulo(serverCount);
-----------------------------------------------------
//传入集群数量 2
private int incrementAndGetModulo(int modulo) {
for (;;) { //不是死循环,而是自旋锁
//默认值是0
int current = nextServerCyclicCounter.get();
//0+1 % 2 = 1
int next = (current + 1) % modulo;
//CAS,JUC中的知识,即比较并替换
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
--------------------------------------------------
//通过求的服务器下标得到对对应的服务器
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
8.5.3 手写
代码核心:负载均衡算法,第几次访问 % 服务实例数量 = index
- 获取服务实例的数量
- 获取第几次访问
步骤:
-
编写自己的LoadBalance接口
负责获得服务实例,后续调用实例的方法
public interface LoadBalance { ServiceInstance instances(List<ServiceInstance> serviceInstances); }
-
编写LoadBalance接口的实现类MyLB
@Component //让Spring扫描到进入容器 public class MyLB implements LoadBalance{ //原子值,初始化为0 private AtomicInteger atomicInteger = new AtomicInteger(0); //获得第几次访问的值 public int RequestNum(){ int current; int next; do{ current = this.atomicInteger.get(); next = current >= 2147483647 ? 0 : current + 1; }while(!this.atomicInteger.compareAndSet(current,next)); //执行一次自旋锁,原子数 + 1 System.out.println("*******第几次访问:"+next); return next; //返回第几次访问的次数 } @Override public ServiceInstance instances(List<ServiceInstance> serviceInstances) { //轮询负载均衡算法,获得实际访问服务器的下标 int index = RequestNum() % serviceInstances.size(); ServiceInstance serviceInstance = serviceInstances.get(index); //返回实际服务器实例 return serviceInstance; } }
测试:
-
注释掉config.ApplicationContextConfig中RestTemplate的
@LoadBalance
注解 -
启动7001/7002
-
修改8001/8002 controller
@GetMapping(value = "/payment/lb") public String getPaymentLB() { return serverPort; }
-
修改order81controller
//注意导包正确 org.springframework.cloud.client.discovery.DiscoveryClient; @Resource private DiscoveryClient discoveryClient; //注入自己的 @Resource private LoadBalance loadBalance; @GetMapping("/consumer/payment/lb") public String lb(){ //通过服务发现得到服务名为CLOUD-PAYMENT-SERVICE的所有实例 List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if(instances == null || instances.size() <= 0){ return null; } ServiceInstance instances1 = loadBalance.instances(instances); URI uri = instances1.getUri(); return restTemplate.getForObject(uri+"/payment/lb",String.class); }
-
http://localhost:81/consumer/payment/lb
九、OpenFeign服务接口调用
9.1 概述
9.1.1 是什么
官网解释:
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
9.1.2 能干嘛
9.1.3 Feign和OpenFeign的区别
9.2 使用步骤
-
建module:cloud-consuemr-openfeign-order81
-
改pom
<!--openfeign--> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
写yML
server: port: 81 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
-
主启动
@SpringBootApplication @EnableFeignClients //在微服务启动的时候,激活OpenFeign public class OpenFeignOrderMain01 { public static void main(String[] args) { SpringApplication.run(OpenFeignOrderMain01.class,args); } }
-
业务类
-
微服务接口
@Component //spring容器可扫描到以注入 @FeignClient(value = "CLOUD-PAYMENT-SERVICE") //使用OpenFeign,对应的微服务名称为:xxx public interface PaymentFeignService { //微服务接口 @GetMapping(value = "/payment/get/{id}") public CommonResult get(@PathVariable("id") Long id); }
-
controller
@RestController @Slf4j public class FeignController { @Autowired private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/get/{id}") public CommonResult get(@PathVariable("id") long id){ //不再通过RestTemplate,调用微服务接口 return paymentFeignService.get(id); } }
-
-
测试
-
总结:
- 服务提供者的controller编写好一个方法
- (服务提供者对外暴露服务接口)在服务消费者端的service层编写微服务接口(
@FeignClient
),微服务名称为服务提供者的spring.application.name,前提是在服务者消费者端的主启动类需要添加OpenFeign激活注解@EnableFiegnClient
- 服务消费者的controller端调用微服务接口
9.3 OpenFeign超时控制
默认OpenFeign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
OpenFeign默认支持Ribbon
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
### OpenFeign超时控制的yaml配置
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000 #5s
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
9.4 OpenFeign日志
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出
9.4.1 日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
9.4.2 配置
-
Config类配置
@Configuration public class FeignConfig { @Bean Logger.Level feignloggerlever(){ return Logger.Level.FULL; } }
-
yaml文件配置
server: port: 81 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ logging: level: # feign日志以什么级别监控哪个接口 com.lebrwcd.springcloud.service.PaymentFeignService: debug
on.run(OpenFeignOrderMain01.class,args);
}
}
5. 业务类
1. 微服务接口
```java
@Component //spring容器可扫描到以注入
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") //使用OpenFeign,对应的微服务名称为:xxx
public interface PaymentFeignService {
//微服务接口
@GetMapping(value = "/payment/get/{id}")
public CommonResult get(@PathVariable("id") Long id);
}
```
2. controller
```java
@RestController
@Slf4j
public class FeignController {
@Autowired
private PaymentFeignService paymentFeignService;
@GetMapping("/consumer/payment/get/{id}")
public CommonResult get(@PathVariable("id") long id){
//不再通过RestTemplate,调用微服务接口
return paymentFeignService.get(id);
}
}
```
[外链图片转存中...(img-10J0m4Ax-1651920441582)]
6. 测试
[外链图片转存中...(img-JIvOjSPs-1651920441583)]
[外链图片转存中...(img-X2HyVaFe-1651920441584)]
7. 总结:
[外链图片转存中...(img-IGvOlRCl-1651920441584)]
- 服务提供者的controller编写好一个方法
- (服务提供者对外暴露服务接口)在服务消费者端的service层编写微服务接口(`@FeignClient`),微服务名称为服务提供者的spring.application.name,前提是在服务者消费者端的主启动类需要添加OpenFeign激活注解`@EnableFiegnClient`
- 服务消费者的controller端调用微服务接口
9.3 OpenFeign超时控制
---
[外链图片转存中...(img-pyYz5jUJ-1651920441585)]
**默认OpenFeign客户端只等待一秒钟**,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
**OpenFeign默认支持Ribbon**
[外链图片转存中...(img-AYGOR8nd-1651920441585)]
```yaml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
### OpenFeign超时控制的yaml配置
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000 #5s
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
9.4 OpenFeign日志
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出
9.4.1 日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
9.4.2 配置
-
Config类配置
@Configuration public class FeignConfig { @Bean Logger.Level feignloggerlever(){ return Logger.Level.FULL; } }
-
yaml文件配置
server: port: 81 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ logging: level: # feign日志以什么级别监控哪个接口 com.lebrwcd.springcloud.service.PaymentFeignService: debug
[外链图片转存中…(img-Uz3PvMuc-1651920441585)]