5. Eureka服务注册与发现
5.1 大纲图
5.2 Eureka基础知识
- 问题:为什么要使用服务注册中心,直接使用80消费者调用8081生成者不就行了???
- 答:单个的消费者调用没有问题,如消费者有很多就需要统一进行管理了。
- 举例:一个病人去私人医院一对一的专家服务,中间不用横着一个门诊挂号。如果病人有很多,那么这个微服务是否还能提供,这个专家还有没有余号,今天到底有多少个病人通过这个专家号,我们需要监控权限流量的管控等等,这时医院就需要有一个门诊即服务注册中心。
5.2.1 什么是服务治理
-
Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理
-
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
5.2.2 什么是服务注册
-
Eureka采用了CS的设计架构,Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
-
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息(接口地址)
5.2.3 Eureka两组件
1)Eureka Server提供服务注册服务
- 各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
- eg:向物业公司交钱填写小区的基本信息
2)EurekaClient通过注册中心进行访问
- 是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
- eg:物业公司收到钱后才会为小区提供服务,如果之后在几个月内没有收到后续的钱则不在为此小区提供服务。
5.3 单机Eureka构建步骤
5.3.1 现在系统的架构
- 一个消费者,一个生产者,一个公共项目,一个服务注册中心。
5.3.2 IDEA生成eurekaServer端服务注册中心
- 类似物业公司
1)建Module:cloud-eureka-server7001
2)改POM
- Springcloud第一季和第二季(此视频)对比说明
<!-- eureka新旧版本 -->
<!-- 以前的老版本(2018)-->
<dependency>
<groupid>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- 现在新版本(2020.2)--><!-- 我们使用最新的 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 编写完整的pom.xml文件:这个新建的项目,所以所有依赖都要添加。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</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.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<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>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
4)写YML
- 在resources目录下新建application.yml文件
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己(想注册也可以,不过没必要)
register-with-eureka: false
#false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eurekaServer交互的地址查询服务和注册服务都需要依赖这个地址,就是上面配置的eureka服务端的实例名称和端口号
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
5)主启动
- 在java包下新建com.angenin.springcloud.EurekaMain7001
- 此项目是服务注册中心注册用的,所以并不需要写业务类。
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // 表示它是服务注册中心
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}
6)测试
- 启动此项目,在浏览器输入http://localhost:7001/
5.3.3 EurekaClient端:把8001注册到注册中心
- EurekaClient端cloud-provider-payment8001将注册进EurekaServer成为服务提供者provider,类似尚硅谷学校对外提供授课服务
1)修改8001支付模块项目
- 这里的提供者,还是之前创建的 cloud-provider-payment8001 模块,做如下修改:
2)修改POM
Springcloud第一季和第二季(此视频)对比说明
<!--以前老版本,别再使用-->
<!-- 以前的老版本(2018)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<dependency>
<!--现在新版本,当前使用-->
<!-- 现在新版本(2020.2)--><!-- 我们使用最新的 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<dependency>
- 完整pom文件:这个是修改之前的8001项目依赖已经添加过了,所以这里只需要添加eureka-client依赖。
<?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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-payment8001</artifactId>
<dependencies>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引用自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web场景启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--boot指标监控依赖-->
<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.20</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>
3)修改YML
#微服务建议一定要写服务端口号和微服务名称
server:
#端口号
port: 8001
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
#数据库配置
datasource:
#引入的数据库驱动类型
type: com.alibaba.druid.pool.DruidDataSource
#mysql5.x的没有cj
driver-class-name: com.mysql.jdbc.Driver
#记得先创建数据库
url: jdbc:mysql://localhost:3306/db2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#true表示向注册中心注册自己,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: #入住到哪个主机上面的哪个端口,即设置与 Eureka Server 交互的地址
defaultZone: http://localhost:7001/eureka
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #mapper.xml文件的位置
type-aliases-package: com.angenin.springcloud.entities #所有Entity别名类所在包(所有实体类所在的包)
4)修改主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient //表示这个项目是eureka的客户端。
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
5)测试
- 先要启动EurekaServer注册中心,在启动8001生产者
- 可以看到8001项目成功注册进注册中心。
6)微服务注册名配置说明
- 在yml文件中application.name(8001)就是注册进注册中心时的应用名(7001)。
7)自我保护机制
- 到后续详解
5.3.4 EurekaClient端:把80注册到注册中心
- EurekaClient端cloud-consumer-order80将注册进EurekaServer成为服务消费者consumer,类似来尚硅谷上课消费的各位同学
1)修改80消费者订单模块
- 这里的消费者,还是之前创建的 cloud-consumer-order80 模块,做如下修改:
2)修改pom
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3)修改yml
#访问一个网站时,默认是80端口,给用户80端口,用户就可以不用加端口直接访问页面
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
4)修改主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
5)测试
- 先要启动EurekaServer,7001服务
- 再要启动服务提供者provider,8001服务
- 测试查询:
http://localhost/consumer/payment/get/4
,仍然可以查询成功。
5.4 集群Eureka构建步骤
5.4.1 Eureka集群原理说明
- 问题:微服务RPC远程服务调用最核心的是什么
- 高可用,试想你的注册中心只有一个only one, 它出故障了那就呵呵( ̄▽ ̄)"了,会导致整个为服务环境不可用。
- 解决办法:搭建Eureka注册中心集群 ,实现负载均衡+故障容错
- 自己理解Eureka集群:相互注册,相互守望
5.4.2 EurekaServer集群环境构建步骤
1)建Module:cloud-eureka-server7002
2)写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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-eureka-server7002</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.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<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>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)修改映射配置hosts
- 现在是2台机器(Eureka),每台机器需要在配置文件配置自己服务端的主机名,因为是本地都叫做localhost,此时2台机器配置的都是localhost那么重名无法区分了,所以需要通过修改hosts文件进行区分,让127.0.0.1映射到2个不同的域名。
- 因为现在真实的物理机器只有一台笔记本,所以我们用不同的端口号来映射同一个地址,7001模拟的是1号机,7002模拟的是2号机。
#Eureka集群配置
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
4)写YML(以前单机)
以前的单机配置形式:
- eureka服务端的实例名称:写的是localhost
- 地址:写的是自己项目配置文件中设置的ip和端口号。
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己(想注册也可以,不过没必要)
register-with-eureka: false
#false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eurekaServer交互的地址查询服务和注册服务都需要依赖这个地址,就是上面配置的eureka服务端的实例名称和端口号
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
现在集群配置方式2台都要修改:相互注册
-
eureka服务端的实例名称:写的是hosts文件中设置的2个不同的域名来区分的
-
地址:写的是另一个项目配置文件中设置的ip和端口号。
-
修改集群的第一台Eureka配置文件
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己(想注册也可以,不过没必要)
register-with-eureka: false
#false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eurekaServer交互的地址查询服务和注册服务都需要依赖这个地址,就是上面配置的eureka服务端的实例名称和端口号
defaultZone: http://eureka7002.com:7002/eureka/
- 修改集群的第二台Eureka配置文件
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己(想注册也可以,不过没必要)
register-with-eureka: false
#false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eurekaServer交互的地址查询服务和注册服务都需要依赖这个地址,就是上面配置的eureka服务端的实例名称和端口号
defaultZone: http://eureka7001.com:7001/eureka/
5)主启动
- 添加7002的主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // 表示它是服务注册中心
public class EurekaMain7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7002.class, args);
}
}
6)测试
-
http://eureka7001.com:7001
,访问第一台发现1指向了2
-
http://eureka7002.com:7002
,访问第二台发现2指向了1。
5.4.3 将支付服务8001微服务发布到上面2台Eureka集群配置中
修改yml配置文件
- 以前单机版
- 现在集群版
#微服务建议一定要写服务端口号和微服务名称
server:
#端口号
port: 8001
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
#数据库配置
datasource:
#引入的数据库驱动类型
type: com.alibaba.druid.pool.DruidDataSource
#mysql5.x的没有cj
driver-class-name: com.mysql.jdbc.Driver
#记得先创建数据库
url: jdbc:mysql://localhost:3306/db2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#true表示向注册中心注册自己,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: #入住到哪个主机上面的哪个端口,即设置与 Eureka Server 交互的地址
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #mapper.xml文件的位置
type-aliases-package: com.angenin.springcloud.entities #所有Entity别名类所在包(所有实体类所在的包)
5.4.4 将订单服务80微服务发布到上面2台Eureka集群配置中
修改yml配置文件
-
以前单机版
-
现在集群版
#访问一个网站时,默认是80端口,给用户80端口,用户就可以不用加端口直接访问页面
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
5.4.5 测试01
-
先要启动EurekaServer,7001/7002服务
-
再要启动服务提供者provider,8001
-
再要启动消费者,80
-
http://eureka7001.com:7001
,发现服务注册到集群
-
http://eureka7002.com:7002
,同样也发现注册到集群
-
http://localhost/consumer/payment/get/4
,查询成功。
-
现在完成的架构:
- 2个注册中心组成的集群相互注册,生产者调用消费者并且都将服务发布到了注册中心。
- 2个注册中心组成的集群相互注册,生产者调用消费者并且都将服务发布到了注册中心。
5.4.6 支付服务提供者8001集群环境构建
- 即:增加第二台生产者项目,内容和第一台8001的的
1)新建cloud-provider-payment8002
2)改POM
- 添加的依赖和8001保持一致
<?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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-payment8002</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引用自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web场景启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--boot指标监控依赖-->
<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.20</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>
3)写YML
- 复制8001的yml文件,修改端口号为8002
#微服务建议一定要写服务端口号和微服务名称
server:
#端口号
port: 8002
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
#数据库配置
datasource:
#引入的数据库驱动类型
type: com.alibaba.druid.pool.DruidDataSource
#mysql5.x的没有cj
driver-class-name: com.mysql.jdbc.Driver
#记得先创建数据库
url: jdbc:mysql://localhost:3306/db2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#true表示向注册中心注册自己,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: #入住到哪个主机上面的哪个端口,即设置与 Eureka Server 交互的地址
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #mapper.xml文件的位置
type-aliases-package: com.angenin.springcloud.entities #所有Entity别名类所在包(所有实体类所在的包)
4)主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient //表示这个项目是eureka的客户端。
@SpringBootApplication
public class PaymentMain8002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8002.class, args);
}
}
5)业务类
- 复制8001的业务类:controller,service,Dao,mapper
- 复制后的目录
6)修改8001/8002的Controller
- 因为现在是2台生产者组成的一个集群,所以对外暴露的名字一致。
- 那么消费者80到底调用的是,生产者集群服务(8001,8002)中的哪一个呢???
- 可以使用@value注解将配置文件的端口号注入到代码中打印出来,这样在调用的时候根据端口号就可以知道具体使用的是哪台生产者。
- 2台生产者控制层都要修改
package com.angenin.springcloud.controller;
import com.angenin.springcloud.entities.CommonResult;
import com.angenin.springcloud.entities.Payment;
import com.angenin.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@Slf4j //日志
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;//添加serverPort
//前后端分离,所以不能直接返回对象,数据要先经过CommonResult封装再返回
@PostMapping("/payment/create")
public CommonResult<Payment> create(@RequestBody Payment payment){
int result = paymentService.create(payment);
log.info("******插入的数据为:" + payment);
log.info("******插入结果:" + result);
if(result > 0){
//插入成功
return new CommonResult(200, "插入数据库成功,serverPort:"+serverPort, result);
}else{
return new CommonResult(444, "插入数据库失败,serverPort:"+serverPort,null);
}
}
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
Payment payment = paymentService.getPaymentById(id);
log.info("******查询结果:" + payment);
if(payment != null){
//查询成功
return new CommonResult(200, "查询成功,serverPort:"+serverPort, payment);
}else{
return new CommonResult(444, "没有对应记录,查询ID:" + id,null);
}
}
}
7)测试
- 先启动7001、7002
- 在启动8001/8002
- 最后启动80
效果 - 访问2台机器的Eureka管理页面(
http://eureka7001.com:7001/、http://eureka7002.com:7002/
)
- 通过消费者80调用生产者集群,可以看到每次调用的都是8001生产者,以为我们之前在消费者80中的控制层代码写死了url地址。(
http://localhost/consumer/payment/get/4
)
- 解决:单机版写的是url地址,现在是集群对外暴露的是统一的服务名称,所以要把url修改为集群的服务名称。
- 即:之前在配置文件设置的服务名称
package com.angenin.springcloud.controller;
import com.angenin.springcloud.entities.CommonResult;
import com.angenin.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderController {
//public static final String PAYMENT_URL = "http://localhost:8001"; 单机
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Resource
private RestTemplate restTemplate;
//因为浏览器只支持get请求,为了方便这里就用get
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment){
log.info("********插入的数据:" + payment);
//postForObject分别有三个参数:请求地址,请求参数,返回的对象类型----写操作
return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
log.info("********查询的id:" + id);
//getForObject两个参数:请求地址,返回的对象类型----读操作
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
- 重新启动80再次测试:
http://localhost/consumer/payment/get/4
- 结果会出现一个异常,原因是现在生产者集群对外是暴漏的微服务名称,但是它并不能识别具体使用的是集群下面的哪个具体的生产者提供的服务。
- 解决:使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
- 查看5.4.7 负载均衡
5.4.7 负载均衡
package com.angenin.springcloud.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
//往容器中添加一个RestTemplate
//RestTemplate提供了多种便捷访问远程http访问的方法
@Bean
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
5.4.8 测试02
- 因为修改了80,所以要进行重启,再次访问
http://localhost/consumer/payment/get/4
- 效果:每刷新一次,8001和8002来会切换。
- 说明:默认的负载均衡时是轮询机制。
- 提前说一下这个就是后面要讲的,Ribbon的负载均衡功能,默认是轮询
- Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号,且该服务还有负载功能了。
5.4.9 到目前为止的架构图
- 2个注册中心组成的集群相互注册
- 2个生产者组成的集群并且注册到了注册中心
- 1个消费者注册到了注册中心。
- 消费者80调用生产者8001、8002组成的集群。
5.5 actuator微服务信息完善
- 可配可不配,推荐配上
5.5.1 主机名称:服务名称修改
1)当前问题
- 含有主机名称,按照规范的要求只暴漏服务名,不要出现主机名称。
2)修改cloud-provider-payment8001/8002
#微服务建议一定要写服务端口号和微服务名称
server:
#端口号
port: 8001
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
#数据库配置
datasource:
#引入的数据库驱动类型
type: com.alibaba.druid.pool.DruidDataSource
#mysql5.x的没有cj
driver-class-name: com.mysql.jdbc.Driver
#记得先创建数据库
url: jdbc:mysql://localhost:3306/db2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#true表示向注册中心注册自己,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: #入住到哪个主机上面的哪个端口,即设置与 Eureka Server 交互的地址
#defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
instance: #重点,和client平行
instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #mapper.xml文件的位置
type-aliases-package: com.angenin.springcloud.entities #所有Entity别名类所在包(所有实体类所在的包)
3)修改之后
- 重启8001,8002(
http://eureka7001.com:7001/
)
5.5.2 访问信息有IP信息提示
1)当前问题
-
点击链接,左下角没有ip地址提示
-
新版本的默认带ip显示
-
注意:导入了这2个依赖后,ip完善才有效果
2)修改cloud-provider-payment8001/8002
instance: #重点,和client平行
instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
prefer-ip-address: true #访问路径可以显示ip地址
3)修改之后
5.6 服务发现Discovery
- 对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
- 即:拿到eureka中注册成功的这些微服务的信息,比如主机名称、端口号…
5.6.1 修改cloud-provider-payment8001的Controller
- 以8001为例,8002做法相同这里不在修改。
package com.angenin.springcloud.controller;
import com.angenin.springcloud.entities.CommonResult;
import com.angenin.springcloud.entities.Payment;
import com.angenin.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@Slf4j //日志
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;//添加serverPort
@Resource
private DiscoveryClient discoveryClient; //springframework的DiscoveryClient(不要导错包了)
@GetMapping("/payment/discovery")
public Object discovery(){
//获取服务列表的信息(即:在Eureka中注册过登录好的微服务有哪些,显示所有注册过的微服务名称)
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("*******element:" + element);
}
// 根据微服务的名称进一步获得该微服务的信息
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
//getServiceId服务器id getHost主机名称 getPort端口号 getUri地址
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return this.discoveryClient;
}
//前后端分离,所以不能直接返回对象,数据要先经过CommonResult封装再返回
@PostMapping("/payment/create")
public CommonResult<Payment> create(@RequestBody Payment payment){
int result = paymentService.create(payment);
log.info("******插入的数据为:" + payment);
log.info("******插入结果:" + result);
if(result > 0){
//插入成功
return new CommonResult(200, "插入数据库成功,serverPort:"+serverPort, result);
}else{
return new CommonResult(444, "插入数据库失败,serverPort:"+serverPort,null);
}
}
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
Payment payment = paymentService.getPaymentById(id);
log.info("******查询结果:" + payment);
if(payment != null){
//查询成功
return new CommonResult(200, "查询成功,serverPort:"+serverPort, payment);
}else{
return new CommonResult(444, "没有对应记录,查询ID:" + id,null);
}
}
}
5.6.2 8001主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient //表示这个项目是eureka的客户端。
@SpringBootApplication
@EnableDiscoveryClient //启用发现客户端-后续详解
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
5.6.3 自测
-
先要启动EurekaServer
-
再启动8001主启动类,需要稍等一会儿
-
http://localhost:8001/payment/discovery
-
现在是自测:消费者8001自己访问自己的ip地址,如果想要在客户端80消费者访问只需要对外暴漏这个服务接口地址,那么80就可以通过这样的访问地址得到微服务的各种信息。(不太理解)
5.7 Eureka自我保护
5.7.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
5.7.2 导致原因
-
为什么会产生Eureka自我保护机制?
- 为了防止EurekaClient可以正常运行,但是 与 EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
-
什么是自我保护模式?
- 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,
此时本不应该注销这个微服务
。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
- 综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
- 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,
-
一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存
-
属于CAP里面的AP分支
5.7.3 怎么禁止自我保护
- 默认是开启状态
- 因为现在注册中心和生产者都是集群组成的,每次重新启动都要启动很多服务比较麻烦,所以这里测试的话只修改7001、8001(类似于单机版),之后只需要启动7001,8001测试,这样更加快捷。
1)注册中心eureakeServer端7001
- 出厂默认,自我保护机制是开启的:
eureka.server.enable-self-preservation=true
- 使用
eureka.server.enable-self-preservation = false
可以禁用自我保护模式
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己(想注册也可以,不过没必要)
register-with-eureka: false
#false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eurekaServer交互的地址查询服务和注册服务都需要依赖这个地址,就是上面配置的eureka服务端的实例名称和端口号
#defaultZone: http://eureka7002.com:7002/eureka/ #集群模式
defaultZone: http://eureka7001.com:7001/eureka/ #切换为单机模式,为了方便测试自我保护机制
server: #server与client对齐
#关闭自我保护,默认为true
enable-self-preservation: false
#心跳的间隔时间,单位毫秒
eviction-interval-timer-in-ms: 2000
- 关闭效果:(启动7001注册中心,访问
http://eureka7001.com:7001/
,此时只有7001启动,原因在上面说了为了方便测试)
2)生产者客户端eureakeClient端8001
- 配置
#微服务建议一定要写服务端口号和微服务名称
server:
#端口号
port: 8001
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
#数据库配置
datasource:
#引入的数据库驱动类型
type: com.alibaba.druid.pool.DruidDataSource
#mysql5.x的没有cj
driver-class-name: com.mysql.jdbc.Driver
#记得先创建数据库
url: jdbc:mysql://localhost:3306/db2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#true表示向注册中心注册自己,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: #入住到哪个主机上面的哪个端口,即设置与 Eureka Server 交互的地址
defaultZone: http://localhost:7001/eureka #单机版
#defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版
instance: #重点,和client平行
instance-id: payment8001 #每个提供者的id不同,显示的不再是默认的项目名
prefer-ip-address: true #访问路径可以显示ip地址
#心跳检测与续约时间
#开发时没置小些,保证服务关闭后注册中心能即使剔除服务
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #mapper.xml文件的位置
type-aliases-package: com.angenin.springcloud.entities #所有Entity别名类所在包(所有实体类所在的包)
3) 测试
-
7001和8001都配置完成
-
先启动7001再启动8001(
http://eureka7001.com:7001/
,发现8001服务正确注入到7001注册中心)
-
关闭8001,表示8001服务宕机,那么此时应该从注册中心立即删除。
6. Zookeeper服务注册与发现
6.1 Eureka停止更新了你怎么办
https://github.com/Netflix/eureka/wiki
,查看官网
6.2 SpringCloud整合Zookeeper代替Eureka
- 前提是杨哥讲过的Zookeeper你已经了解清楚并在你的Centos7服务器上面配置成功(采用虚拟机配置)。
6.2.1 注册中心Zookeeper
-
概念:zookeeper是一个分布式协调工具,可以实现注册中心功能
-
zookeeper服务器取代Eureka服务器,zk作为服务注册中心
-
关闭Linux服务器防火墙后启动zookeeper服务器
# 关闭防火墙
systemctl stop firewalld.service
# 禁止防火墙开机启动
systemctl disable firewalld.service
# 查看防火墙状态
systemctl status firewalld.service
# Zookeeper启动详情查看----Zookeeper基础操作 博客
- 测试服务是否ping通
6.2.2 服务提供者
1)新建cloud-provider-payment8004
2)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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</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.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<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>
3)YML
#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 8004
spring:
application:
#服务别名----注册zookeeper到注册中心名称
name: cloud-provider-payment
cloud:
zookeeper:
#linux主机ip+zookeeper端口号
connect-string: 192.168.10.140:2181
4)主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient//该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
5)Controller
- 说明:现在主要学习的是zookeeper作为服务注册中心的整合,所以这里只写个Controller用于测试即可,不再写业务层、数据层、操作数据库。
package com.angenin.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Slf4j
@RestController
public class PaymentController {
@Value("${server.port}") //获取端口号
private String serverPort;
//查询端口号,8004生产者能否注册进入zookeeper,获得端口号
@RequestMapping("/payment/zk")
public String paymentzk(){
return "springcloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString();
}
}
6)启动8004注册进zookeeper
- 首先启动zookeeper(默认端口号是2181)
# 启动Zookeeper服务端
./zkServer.sh start
#启动客户端
# 如果连接本地Zookeeper,那么ip:port可省略
./zkCli.sh -server ip:port
-
在启动8004服务生产者,会产生问题(新版本没有问题23/8/21)
-
解决zookeeper版本jar包冲突问题
- 原因:视频中老师安装的zookeeper的版本是3.4.9版本,而引入zookeeper依赖坐标默认的版本是3.5.3,所以会报jar包冲突。
- 解决:
- 方式一:卸载安装在linux系统中的zookeeper,安装与之版本匹配的zookeeper(不推荐,zookeeper安装完成后不会轻易变动,因为别的系统可能在使用)
- 方式二:使用jar包排除引入
-
排出zk冲突后的新POM.xml
- 由于我学习时安装的是3.5.7版本,没有报错所以这一步可以省略。
- 由于我学习时安装的是3.5.7版本,没有报错所以这一步可以省略。
<?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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</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.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper3.5.3-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.4.9版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</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>
7)验证测试
-
查看是8004是否注册进zookeeper
-
浏览器输入:
http://localhost:8004/payment/zk
8)验证测试2
- 进一步深挖里面的内容:紫色框里面的内容就是zookeeper上面的基本信息
- 把这段内容复制到一个在线的json解析工具中(百度搜)
9)思考:服务节点是临时节点还是持久节点
- 在zookeeper服务器上我们都明白一个东西叫znode节点,每一个微服务作为节点放在zookeeper里面。
- zookeeper节点分类:
- 临时节点
- 带序号的临时节点
- 持久节点
- 带序号的持久节点
- 问题:注册进入zookeeper注册中心的节点是临时节点还是持久节点呢???
- 答:临时节点
- 我们在zk上注册的node是临时节点,当我们的服务一定时间内没有发送心跳,那么zk就会将这个服务的znode删除了。没有自我保护机制。重新建立连接后znode-id号也会变
- 我们在zk上注册的node是临时节点,当我们的服务一定时间内没有发送心跳,那么zk就会将这个服务的znode删除了。没有自我保护机制。重新建立连接后znode-id号也会变
6.2.3 服务消费者
1)新建cloud-consumerzk-order80
2)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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumerzk-order80</artifactId>
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<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>
3)YML
#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 80
spring:
application:
#服务别名----注册zookeeper到注册中心名称
name: cloud-consumer-order
cloud:
zookeeper:
#linux主机ip+zookeeper端口号
connect-string: 192.168.10.140:2181
4)主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class OrderZKMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderZKMain80.class, args);
}
}
5)业务类
- 配置Bean
package com.angenin.springcloud.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
//往容器中添加一个RestTemplate
//RestTemplate提供了多种便捷访问远程http访问的方法
@Bean
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- Controller
package com.angenin.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderZKController {
//调用服务生产者的服务名称
public static final String INVOKE_URL = "http://cloud-provider-payment";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/payment/zk")
public String paymentInfo(){
//getForObject两个参数:请求地址,返回的对象类型----读操作
String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
return result;
}
}
6)验证测试
- 启动消费者80,查看是否注册进入到注册中心
7)访问测试地址
http://localhost/consumer/payment/zk
8)关于Zookeeper集群
- 关于 zookeeper 的集群搭建,目前使用较少,而且在 yml 文件中的配置也是类似,以列表形式写入 zookeeper 的多个地址即可,而且zookeeper 集群,在 zookeeper课程中讲解过。总而言之,只要配合zookeeper集群,以及yml文件的配置就能完成集群搭建
9)当前架构
- 一个zookeeper注册中心、一个生产者8004、一个消费者80,并且80和8004都注册进入了服务注册中心,消费者80调用生产者8004。
7. Consul服务注册与发现
7.1 Consul简介
-
是什么:
https://www.consul.io/intro/index.html
- Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用
Go 语言开发
。 - 提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。
- 它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
-
能干嘛
- 服务发现:提供HTTP和DNS两种发现方式。
- 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化监控
- KV存储:Key、Value的存储方式
- 多数据中心:Consul支持多数据中心
- 可视化Web界面
-
去哪下:选择自己想要的版本
https://www.consul.io/downloads.html
-
怎么玩
https://www.springcloud.cc/spring-cloud-consul.html
-
默认端口号是8500
7.2 安装并运行Consul
-
官网安装说明:
https://learn.hashicorp.com/consul/getting-started/install.html
-
以下载windows版本的Consul为例:
-
下载完成后解压只有一个consul.exe文件,cmd进入到命令行窗口,查看版本号信息
-
使用开发模式启动
-
consul agent -dev
-
通过以下地址可以访问Consul的首页:http://localhost:8500
-
结果页面
-
7.3 服务提供者
7.3.1 新建Module支付服务provider8006
- cloud-providerconsul-payment8006
7.3.2 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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-providerconsul-payment8006</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<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>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
7.3.3 YML
#consul服务端口号
server:
port: 8006
#对外暴露的服务名
spring:
application:
name: consul-provider-payment
#consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${
spring.application.name}
7.3.4 主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //用于启用服务注册与发现功能。
public class PaymentMain8006
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class, args);
}
}
7.3.5 业务类Controller
package com.angenin.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@Slf4j
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/consul")
public String paymentConsul()
{
return "springcloud with consul: "+serverPort+"\t "+ UUID.randomUUID().toString();
}
}
7.3.6 验证测试
-
启动consul服务注册中心,在启动服务生产者8006
-
查看Consul注册中心首页(http://localhost:8500),可以看到生产者已经注册进服务注册中心
-
http://localhost:8006/payment/consul
7.4 服务消费者
7.4.1 新建Module消费服务order80
- cloud-consumerconsul-order80
7.4.2 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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumerconsul-order80</artifactId>
<dependencies>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!--日常通用jar包配置-->
<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>
7.4.3 YML
###consul服务端口号
server:
port: 80
spring:
application:
name: cloud-consumer-order
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${
spring.application.name}
7.4.4 主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class OrderConsulMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
7.4.5 配置Bean
package com.angenin.springcloud.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
7.4.6 Controller
package com.angenin.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderConsulController{
public static final String INVOKE_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping(value = "/consumer/payment/consul")
public String paymentInfo(){
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
return result;
}
}
7.4.7 验证测试
- 启动80消费者
- 打开Consul首页(http://localhost:8500)
7.4.8 访问测试地址
- http://localhost/consumer/payment/consul
7.5 三个注册中心异同点
组件名 | 语言CAP | 服务健康检查 | 对外暴露接口 | Spring Cloud集成 |
---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP |
Consul | Go | CP | 支持 | HTTP/DNS |
Zookeeper | Java | CP | 支持客户端 | 已集成 |
7.5.1 CAP
CAP理论关注粒度是数据,而不是整体系统设计的策略
- C:Consistency(强一致性)
- A:Availability(可用性)
- P:Partition tolerance(分区容错性)
7.5.2 经典CAP图
最多只能同时较好的满足两个。
- CAP理论的核心是:
一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求
,因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:- CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
- CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
- AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
1) AP架构(Eureka)
- 当网络分区出现后,为了保证可用性,系统B
可以返回旧值
,保证系统的可用性。 结论:违背了一致性C的要求,只满足可用性和分区容错,即AP
- Eureka有一种自我保护机制,他强调的是AP保证的是微服务的高可用,好死不如赖活着,即便偶尔宕机掉线了一时半会收不到,也不会立刻删除。
2) CP架构(Zookeeper/Consul)
- 当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP
- 一旦服务停掉,会立刻进行删除。
8 Ribbon负载均衡服务调用
8.1 概述
-
恢复eureka集群环境,以便接下来的练习。
- 之前为了方便测试自我保护机制(每次启动所有的服务比较麻烦),所以只启动了7001服务注册中心和8001生产者,所以这里需要把单击模式的配置恢复为集群方式的配置。(修改7001yml文件为集群配置方式,修改8001yml配置文件为集群方式)
- 之前为了方便测试自我保护机制(每次启动所有的服务比较麻烦),所以只启动了7001服务注册中心和8001生产者,所以这里需要把单击模式的配置恢复为集群方式的配置。(修改7001yml文件为集群配置方式,修改8001yml配置文件为集群方式)
-
启动7001、7002服务注册中心集群
-
启动8001、8002生产者集群
-
启动80消费者集群
-
消费者80调用生产者集群
-
访问Eureka7001的首页:
http://eureka7001.com:7001/
-
访问Eureka7002的首页:
http://eureka7002.com:7002/
8.1.1 是什么
- Spring Cloud Ribbon是基于Netflix Ribbon实现的一套
客户端负载均衡的工具
。 - 简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供
客户端的软件负载均衡算法和服务调用
。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
8.1.2 官网资料
-
https://github.com/Netflix/ribbon/wiki/Getting-Started
-
Ribbon目前也进入维护模式
-
未来替换方案
- Ribbon未来可能被Spring Cloud LoadBalacer替代。
8.1.3 能干吗
-
LB负载均衡(Load Balance)是什么
- 简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
- 常见的负载均衡有软件Nginx,LVS,硬件 F5等。
-
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
- Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
- Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
-
LB(负载均衡)
- 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方;
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB
,它只是一个类库,集成于消费方进程
,消费方通过它来获取到服务提供方的地址。
-
前面我们讲解过了80通过轮询负载访问8001/8002(默认就是轮询)
-
一句话:
负载均衡+RestTemplate调用
- 客户端的负载均衡工具,配合RestTemplate实现RPC的远程调用
8.2 Ribbon负载均衡演示
8.2.1 架构说明
- Ribbon在工作时分成两步
- 第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
- 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
- 其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
- 总结:Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
8.2.2 POM
- 解释:之前没有引入Ribbon依赖,为什么可以做到负载均衡。
- 是否引入Ribbon依赖
<!--Ribbon的依赖:因为下面这个eureka-client依赖已经只带了Ribbon的依赖,所以此依赖加不加都可以-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
8.2.3 二说RestTemplate的使用
-
官网
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
-
getForObject方法/getForEntity方法
-
postForObject/postForEntity
-
GET请求方法(读)
-
POST请求方法(写)
-
测试:使用getForEntity方法测试查询
//使用getForEntity方法测试查询
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
//getStatusCode获取状态码,is2xxSuccessful如果是状态码是200代表成功
if(entity.getStatusCode().is2xxSuccessful()){
//如果成功了返回请求体
return entity.getBody();
}else{
return new CommonResult<>(444,"操作失败");
}
}
- 重启80,发现使用getForEntity方法测试查询仍然成功
8.3 Ribbon核心组件IRule
8.3.1 IRule接口
- IRule:根据特定算法中从服务列表中选取一个要访问的服务
- IRule主要的实现类(Ribbon自带的有7种负载均衡的算法)
- com.netflix.loadbalancer.RoundRobinRule:轮询
- com.netflix.loadbalancer.RandomRule:随机
- com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器
8.3.2 如何替换
1)修改cloud-consumer-order80
2)注意配置细节
官方文档明确给出了警告:
- 这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
- 即:主启动类会加上@SpringBootApplication注解,这个注解底层包含@ComponentScan,作用是默认扫描主启动类所在的包以及所在的子包中。现在要求是Ribbon这个配置类不能放在主启动类所在的包以及所在的子包中,所以要新建个包。
3)新建package :com.angenin.myrule
4)上面包下新建MySelfRule规则类
package com.angenin.myrule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MySelfRule {
@Bean
public IRule myrule(){
return new RandomRule(); //负载均衡规则定义为随机
}
}
5)主启动类添加@RibbonClient
package com.angenin.springcloud;
import com.angenin.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* 在启动该微服务的时候就能去加载我们的自定义Ribbon配置类,从而使配置生效,形如:
* 服务生产者的服务名(配置文件是小写,这里是大写)、配置类的类型
*/
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)
@EnableEurekaClient
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
6)测试
-
启动7001、7002服务注册中心集群
-
启动8001、8002生产者集群
-
启动80消费者集群
-
消费者80调用生产者集群
-
http://localhost/consumer/payment/get/4(显示结果是随机的,不再是轮询的8001和8002来回替换)
8.4 Ribbon负载均衡算法
- 恢复成默认轮询的规则
8.4.1 轮询规则的原理
- 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,
每次服务重启动后rest接口计数从1开始
。 - 获取服务列表的集群数:
List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
- 如:
List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
- 8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:
- 当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
- 当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
- 当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
- 当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
- 如此类推…
8.4.2 RoundRobinRule轮询规则的源码
- ctrl+n打开IRule接口
- ctrl+alt+b:查看此接口的实现类
- 源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.netflix.loadbalancer;
import com.netflix.client.config.IClientConfig;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RoundRobinRule extends AbstractLoadBalancerRule {
//AtomicInteger原子整形类
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
//此时nextServerCyclicCounter是一个原子整形类,并且value为0
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
//ILoadBalancer选择哪一个负载均衡机制,这里lb为轮询
public Server choose(ILoadBalancer lb, Object key) {
//如果传入的lb没有负载均衡,为空,那么报错
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
//还没选到执行的server,并且选择的次数没超过10次,进行选择server
while(true) {
if (server == null && count++ < 10) {
//lb.getReachableServers获取所有状态是up的服务实例
List<Server> reachableServers = lb.getReachableServers();
//lb.getAllServers获取所有服务实例
List<Server> allServers = lb.getAllServers();
//状态为up的服务实例的数量
int upCount = reachableServers.size();
//所有服务实例的数量
int serverCount = allServers.size();
//如果up的服务实例数量为0或者服务实例为0,打印日志log.warn并返回server=null
if (upCount != 0 && serverCount != 0) {
//获取到接下来server的下标
int nextServerIndex = this.incrementAndGetModulo(serverCount);
//获取下一个server
server = (Server)allServers.get(nextServerIndex);
//如果
if (server == null) {
//线程让步,线程会让出CPU执行权,让自己或者其它的线程运行。(让步后,CPU的执行权也有可能又是当前线程)
Thread.yield();
} else {
//获取的server还活着并且还能工作,则返回该server
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
//否则server改为空
server = null;
}
//进入下次循环
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//选择次数超过10次,打印日志log.warn并返回server=null
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
private int incrementAndGetModulo(int modulo) {
int current;
int next;
//CAS加自旋锁
//CAS(Conmpare And Swap):是用于实现多线程同步的原子指令。CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
//自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
do {
//获取value,即0
current = this.nextServerCyclicCounter.get();
//取余,为1
next = (current + 1) % modulo;
//进行CAS判断,如果此时在value的内存地址中,如果value和current相同,则为true,返回next的值,否则就一直循环,直到结果为true
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
8.4.3 手写:自己试着写一个本地负载均衡器试试(轮询)
- 前提:原理+JUC(CAS+自旋锁的复习)
- juc视频:https://www.bilibili.com/video/BV1ar4y1x727/?spm_id_from=333.999.0.0&vd_source=47cc8ff7e1b2b25a9a062c51f8b85d17
1)7001/7002集群启动
- 启动7001、7002集群
2)8001/8002微服务改造:controller
- 在8001和8002的PaymentController中加上这个方法,用于测试我们的自定义轮询:
//用于测试自定义负载均衡的规则
@GetMapping("/payment/lb")
public String getPaymentLB(){
return serverPort;
}
3)80订单微服务改造
-
ApplicationContextBean去掉注解@LoadBalance
- 去掉Ribbon自带的负载均衡,如果起效果了说明自己写的负载均衡配置成功。
- 去掉Ribbon自带的负载均衡,如果起效果了说明自己写的负载均衡配置成功。
-
LoadBalancer接口
package com.angenin.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
public interface LoadBalancer {
//传入具体实例的集合,返回选中的实例
ServiceInstance instances(List<ServiceInstance> serviceInstance);
}
- MyLB接口实现类
package com.angenin.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component //加入容器
public class MyLB implements LoadBalancer
{
//新建一个原子整形类
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
//如果current是最大值,重新计算,否则加1(防止越界)
next = current >= 2147483647 ? 0 : current + 1;
//进行CAS判断,如果不为true,进行自旋
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("*****第几次访问,次数next: "+next);
return next;
}
//负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances)
{
//进行取余
int index = getAndIncrement() % serviceInstances.size();
//返回选中的服务实例
return serviceInstances.get(index);
}
}
- OrderController
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;
//测试自己写的负载均衡
@GetMapping("/consumer/payment/lb")
public String getPaymentLB(){
//获取CLOUD-PAYMENT-SERVICE服务的所有具体实例
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() <= 0){
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
System.out.println(uri);
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
- 测试:http://localhost/consumer/payment/lb
- 先启动服务
- 效果:自己写的轮询负载规则测试成功
- 先启动服务
- 控制台
9 OpenFeign服务接口调用
9.1 概述
9.1.1 OpenFeign是什么
官网文档解释:
-
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
-
Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
-
它的使用方法是
定义一个服务接口然后在上面添加注解
。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡
总结:
- Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,
只需创建一个接口并在接口上添加注解即可
- GitHub源码:
https://github.com/spring-cloud/spring-cloud-openfeign
9.1.2 能干嘛
-
Feign能干什么
- Feign旨在使编写Java Http客户端变得更容易。
- 前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,
往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用
。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可
),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
-
Feign集成了Ribbon
- 利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,
通过feign只需要定义服务绑定接口且以声明式的方法
,优雅而简单的实现了服务调用
- 利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,
9.1.3 Feign和OpenFeign两者区别
9.2 OpenFeign使用步骤
- 微服务调用接口+@FeignClient
- 微服务调用接口:提供方和调用方相吻合的接口
- Feign在消费端使用
- 架构
9.2.1 新建cloud-consumer-feign-order80
9.2.2 POM
- 可以看到老版本的OpenFeign里面同样整合了Ribbon,新版本没有。
<?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>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-order80</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</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-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>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
9.2.3 YML
- OpenFeign就不把它作为微服务注册进eureka了, 就是个客户端。
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url: # 配置服务中心,openFeign去里面找服务
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
9.2.4 主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients //使用feign,激活并开启
@SpringBootApplication
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
9.2.5 业务类
1)新建PaymentFeignService接口并新增注解@FeignClient
- 业务逻辑接口+@FeignClient配置调用provider服务
package com.angenin.springcloud.service;
import com.angenin.springcloud.entities.CommonResult;
import com.angenin.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
//找到注册中心上的微服务接口名
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
//调用8001的控制层方法
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
2)控制层Controller
package com.angenin.springcloud.controller;
import com.angenin.springcloud.entities.CommonResult;
import com.angenin.springcloud.entities.Payment;
import com.angenin.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id);
}
}
9.2.6 测试
- 先启动2个eureka集群7001/7002
- 再启动2个微服务8001/8002
- 启动cloud-consumer-feign-order80
- http://localhost/consumer/payment/get/4
- Feign自带负载均衡配置项Ribbon
- Feign自带负载均衡配置项Ribbon
9.2.7 小总结
9.3 OpenFeign超时控制
- 说明:消费者调用生产者这是2个不同的微服务,所以一定会存在一种现象超时。
- eg:提供者在处理服务时用了3秒,提供者认为花3秒是正常,而消费者只愿意等1秒,1秒后,提供者会没返回数据,消费者就会造成超时调用报错。
- 所以需要双方约定好时间,不使用默认的。
9.3.1 超时设置,故意设置超时演示出错情况
- 服务提供方8001故意写暂停程序
//测试OpenFeign超时控制
@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
- 服务消费方80添加超时方法PaymentFeignService
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout();
- 服务消费方80添加超时方法OrderFeignController
@GetMapping("/consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
//openFeign底层是---ribbon,客户端(消费者)一般默认等待1秒
return paymentFeignService.paymentFeignTimeout();
}
- 测试
-
启动7001、7002,提供者8001,消费者cloud-consumer-feign-order80
-
http://localhost/consumer/payment/feign/timeout
-
错误页面
-
9.3.2 是什么
- OpenFeign默认等待1秒钟,超过后报错
- 默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
- OpenFeign默认支持Ribbon(3开始的版本剔除了),它的超时控制也由最底层的Ribbon来进行限制。
9.3.3 YML文件里需要开启OpenFeign客户端超时控制
#设置feign客户端超时时间(OpenFeign默认支持ribbon)(单位:毫秒)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ReadTimeout: 5000
- 重启80进行测试:http://localhost/consumer/payment/feign/timeout
9.4 OpenFeign日志打印功能
- Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出
- 日志级别:
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
测试步骤:
- 配置日志bean
package com.angenin.springcloud.config;
import feign.Logger; //不要导错包
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
//打印最详细的日志
return Logger.Level.FULL;
}
}
- YML文件里需要开启日志的Feign客户端
#开启日志的feign客户端
logging:
level:
#feign日志以什么级别监控哪个接口
com.angenin.springcloud.service.PaymentFeignService: debug #写你们自己的包名
- 后台日志查看:重启80