一、前言
接下来是开展一系列的 SpringCloud 的学习之旅,从传统的模块之间调用,一步步的升级为 SpringCloud 模块之间的调用,此篇文章为第二篇,即使用服务注册和发现的组件,此篇文章会介绍 Eureka、Zookeeper 和 Consul 分别作为注册中心的使用场景。
二、Eureka 服务注册与发现
2.1 Eureka 基础知识
2.1.1 什么是服务治理
SpringCloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。
在传统的 rpc 远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
2.1.2 什么是服务注册
Eureka 采用了 CS 的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息 比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地 RPC 调用 RPC 远程调用。
框架核心设计思想在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何 rpc 远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))。
2.1.3 Eureka 两组件
Eureka 包含两个组件:Eureka Server 和 Eureka Client。
EurekaServer 提供服务注册服务各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
EurekaClient 通过注册中心进行访问是一个 Java 客户端,用于简化 EurekaServer 的交互,客户端同时也具备一个内置的、使用轮询 (round-robin) 负载算法的负载均衡器。在应用启动后,将会向 Eureka Server 发送心跳(默认周期为 30 秒)。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,EurekaServer 将会从服务注册表中把这个服务节点移除(默认 90 秒)。
2.2 单机 Eureka 构建
2.2.1 修改支付模块
修改 cloud-provider-payment8001 支付模块,使其成为 EurekaClient 端,并将其注册到 EurekaServer 端中,在 pom.xml 中,添加 eureka 客户端的依赖,如下:
<!--eureka-client-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在 application.yml 中添加 eureka 的配置信息,如下:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:defaultZone: http://localhost:7001/eureka
在启动类上添加 eureka 客户端的注解类,如下:
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8081 {public static void main(String[] args) {SpringApplication.run(PaymentMain8081.class,args);}
}
2.2.2 修改订单模块
修改 cloud-consumer-order80 订单模块,使其成为 EurekaClient 端,并将其注册到 EurekaServer 端中,在 pom.xml 中,添加 eureka 客户端的依赖,如下:
<!--eureka-client-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在 application.yml 中添加 eureka 的配置信息,如下:
server:port: 80spring:application:name: cloud-order-serviceeureka:client:#表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:defaultZone: http://localhost:7001/eureka
在启动类上添加 eureka 客户端的注解类,如下:
@SpringBootApplication
@EnableEurekaClient
public class OrderMain80 {public static void main(String[] args) {SpringApplication.run(OrderMain80.class,args);}
}
2.2.3 创建 Eureka7001 模块
创建 cloud-eureka-server7001 模块,使其成为 eurekaServer 端服务注册中心,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-eureka-server7001</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--eureka-server--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><dependency><groupId>com.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</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>
application.yml 的内容如下所示:
server:port: 7001eureka:instance:hostname: localhost # eureka服务端的实例名称client:# false 表示不向注册中心注册自己。register-with-eureka: false# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务fetch-registry: falseservice-url:# 设置与 Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类的代码如下所示,
package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication
// eureka 服务端的注解
@EnableEurekaServer
public class EurekaMain7001 {public static void main(String[] args){SpringApplication.run(EurekaMain7001.class,args);}
}
2.2.4 测试
分别启动 cloud-provider-payment8001、cloud-consumer-order80 和 cloud-eureka-server7001 模块,在浏览器输入 http://localhost:7001,如下图,可以看到,注册成功了。
2.3 集群 Eureka 构建
2.3.1 原理说明
服务注册:将服务信息注册进注册中心。
服务发现:从注册中心上获取服务信息。实质上是存储的是 key 和 value,key 为服务名称,value 为服务地址。
2.3.2 微服务远程调用核心
微服务 RPC 远程服务调用最核心的是什么?答案为高可用,假设你的注册中心只有一个,如果它发生故障,就会导致整个为服务环境不可用。解决办法是搭建 Eureka 注册中心集群,实现负载均衡 + 故障容错
2.3.3 创建 Eureka7002 模块
创建 cloud-eureka-server7002 模块,使其成为 eurekaServer 端服务的第二个注册中心,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-eureka-server7002</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--eureka-server--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><dependency><groupId>com.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</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>
启动类的代码如下所示,
package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication
// eureka 服务端的注解
@EnableEurekaServer
public class EurekaMain7002 {public static void main(String[] args){SpringApplication.run(EurekaMain7002.class,args);}
}
2.3.4 修改配置映射
找到 C:\Windows\System32\drivers\etc 目录下的 hosts 文件,添加映射配置到 hosts 文件中,如下图:
2.3.5 修改 yml 文件
修改 cloud-eureka-server7001 模块的 application.yml 文件,使其注册到 7002 服务端上,内容如下:
server:port: 7001eureka:instance:hostname: eureka7001.com # eureka服务端的实例名称client:# false 表示不向注册中心注册自己。register-with-eureka: false# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务fetch-registry: falseservice-url:# 三台节点的写法# defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/# 设置与 Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。defaultZone: http://eureka7002.com:7002/eureka/
修改 cloud-eureka-server7002 模块的 application.yml 文件,使其注册到 7001 服务端上,内容如下:
server:port: 7002eureka:instance:hostname: eureka7002.com # eureka服务端的实例名称client:# false 表示不向注册中心注册自己。register-with-eureka: false# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务fetch-registry: falseservice-url:# 三台节点的写法# defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/# 设置与 Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。defaultZone: http://eureka7001.com:7001/eureka/
2.3.6 测试
启动 cloud-eureka-server7001 和 cloud-eureka-server7002 模块,在浏览器访问下,如下图,可以看到,互相注册成功了。
2.3.7 将支付模块注册到集群
将支付模块 cloud-provider-payment8001 发布到上面 2 台 Eureka 集群配置中,修改该模块的 application.yml,内容如下所示:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
2.3.8 将订单模块注册到集群
将支付模块 cloud-consumer-order80 发布到上面 2 台 Eureka 集群配置中,修改该模块的 application.yml,内容如下所示:
eureka:client:#表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:#defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
2.3.9 测试
先启动 EurekaServer 的 7001 和 7002 服务,然后再启动服务提供者 payment8001 模块,最后再启动消费者 order80 模块,然后访问 EurekaServer 的管理界面,如下所示,可以看到两个模块均注册成功了。
并且服务可以正常的提供调用服务,如下图:
2.3.10 构建支付模块集群
现在的支付模块只有一个 cloud-provider-payment8001,如果它要是宕机了,整个系统也就无法提供服务了,所以我们需要构建支付模块的集群,新建一个子模块 cloud-provider-payment8002。
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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-provider-payment8002</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--eureka-client--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.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.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>
application.yml 的内容如下所示:
server:port: 8001spring:application:name: cloud-payment-servicedatasource:type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.gjt.mm.mysql.Driver url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: 123456
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版mybatis:mapperLocations: classpath:mapper/*.xmltype-aliases-package: com.springcloud.entities
主启动类的内容如下所示:
package com.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8082 {public static void main(String[] args) {SpringApplication.run(PaymentMain8082.class,args);}
}
其他的业务类都从 cloud-provider-payment8001 模块粘贴即可,都是一样的代码。
修改 cloud-provider-payment8001 模块的 PaymentController 代码,添加一些可以标识出调用信息出自哪个端口的代码,如下:
@RestController
@Slf4j
public class PaymentController {@Value("${server.port}")private String serverPort;@ResourcePaymentService paymentService;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("*****插入操作返回结果:" + result);if(result >0){return new CommonResult(200,"插入成功,返回结果"+result+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"插入失败",null);}}@GetMapping("/payment/get/{id}")public CommonResult<Payment> selectPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.selectPaymentById(id);log.info("*****查询操作返回结果:" + payment);if(payment != null){int a =2;return new CommonResult(200,"查询成功"+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"未查询到数据"+id,null);}}
}
修改 cloud-provider-payment8002 模块的 PaymentController 代码,添加一些可以标识出调用信息出自哪个端口的代码,如下:
@RestController
@Slf4j
public class PaymentController {@Value("${server.port}")private String serverPort;@ResourcePaymentService paymentService;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("*****插入操作返回结果:" + result);if(result >0){return new CommonResult(200,"插入成功,返回结果"+result+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"插入失败",null);}}@GetMapping("/payment/get/{id}")public CommonResult<Payment> selectPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.selectPaymentById(id);log.info("*****查询操作返回结果:" + payment);if(payment != null){int a =2;return new CommonResult(200,"查询成功"+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"未查询到数据"+id,null);}}
}
修改 cloud-consumer-order80 订单模块的 OrderController 类,因为以前这里面服务调用的地址是写死的,现在需要写上如下的地址
@RestController
@Slf4j
public class OrderController {@Resourceprivate RestTemplate restTemplate;// public static final String PaymentSrv_URL = "http://localhost:8001";public static final String PAYMENT_SRV = "http://CLOUD-PAYMENT-SERVICE";@GetMapping("/consumer/payment/create")public CommonResult create(Payment payment){// 客户端用浏览器是get请求,但是底层实质发送post调用服务端8001return restTemplate.postForObject(PAYMENT_SRV+"/payment/create",payment,CommonResult.class);}@GetMapping("/consumer/payment/get/{id}")public CommonResult getPayment(@PathVariable Long id){return restTemplate.getForObject(PAYMENT_SRV + "/payment/get/"+id, CommonResult.class, id);}
}
修改 cloud-consumer-order80 订单模块的 ApplicationContextConfig 类,给 RestTemplate 添加负载均衡的能力,如下图:
@Configuration
public class ApplicationContextConfig {@Bean//使用@LoadBalanced注解赋予RestTemplate负载均衡的能力@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}
}
先启动 EurekaServer 的 7001 和 7002 服务,然后再启动服务提供者 payment8001 和 payment8002 模块,最后再启动消费者 order80 模块,然后调用方法进行测试,如下图:
2.4 actuator 微服务信息完善
2.4.1 服务名称修改
访问 Eureka 的管理界面,可以看到里面包含了主机的名称,这个是我们不想要的,如下图:
那么如何才能不显示我们的主机名称呢?只需要配置一些信息即可。修改 cloud-provider-payment8001 模块的 application.yml,内容如下所示:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版instance:# 设置名称instance-id: payment8001
修改 cloud-provider-payment8002 模块的 application.yml,内容如下所示:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版instance:# 设置名称instance-id: payment8002
重启这两个模块,然后刷新 Eureka 的管理界面,如下图,可以看到,不再显示我们的主机名称。
2.4.2 显示 ip 信息
我们想实现当鼠标放到服务的名称上时显示 ip 信息,现在的效果如下所示:
那么如何才能显示我们的 ip 信息呢?只需要配置一些信息即可。修改 cloud-provider-payment8001 模块的 application.yml,内容如下所示:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版instance:# 设置名称instance-id: payment8001# 访问路径可以显示IP地址prefer-ip-address: true
修改 cloud-provider-payment8002 模块的 application.yml,内容如下所示:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版instance:# 设置名称instance-id: payment8002# 访问路径可以显示IP地址prefer-ip-address: true
重启这两个模块,然后刷新 Eureka 的管理界面,如下图,可以看到,当鼠标放上去的时候会显示我们的 ip 地址信息。
2.5 服务发现 Discovery
如果我们想获取在 Eureka 上注册的微服务的对外暴露信息,我们该如何获取呢?比如说主机名称、端口号等。其实对于注册进 Eureka 里面的微服务,可以通过服务发现来获得该服务的信息。
修改 cloud-provider-payment8001 模块的 PaymentController 代码,内容如下所示:
@RestController
@Slf4j
public class PaymentController {@Value("${server.port}")private String serverPort;@ResourcePaymentService paymentService;@Resourceprivate DiscoveryClient discoveryClient;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("*****插入操作返回结果:" + result);if(result >0){return new CommonResult(200,"插入成功,返回结果"+result+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"插入失败",null);}}@GetMapping("/payment/get/{id}")public CommonResult<Payment> selectPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.selectPaymentById(id);log.info("*****查询操作返回结果:" + payment);if(payment != null){int a =2;return new CommonResult(200,"查询成功"+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"未查询到数据"+id,null);}}// 对外提供一个获取服务的所有方法@GetMapping(value = "/payment/discovery")public Object discovery(){List<String> services = discoveryClient.getServices();for (String element : services) {System.out.println(element);}List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");for (ServiceInstance element : instances) {System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t"+ element.getUri());}return this.discoveryClient;}
}
修改 cloud-provider-payment8002 模块的 PaymentController 代码,内容如下所示:
@RestController
@Slf4j
public class PaymentController {@Value("${server.port}")private String serverPort;@ResourcePaymentService paymentService;@Resourceprivate DiscoveryClient discoveryClient;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("*****插入操作返回结果:" + result);if(result >0){return new CommonResult(200,"插入成功,返回结果"+result+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"插入失败",null);}}@GetMapping("/payment/get/{id}")public CommonResult<Payment> selectPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.selectPaymentById(id);log.info("*****查询操作返回结果:" + payment);if(payment != null){int a =2;return new CommonResult(200,"查询成功"+"服务端口:"+serverPort,payment);}else{return new CommonResult(404,"未查询到数据"+id,null);}}// 对外提供一个获取服务的所有方法@GetMapping(value = "/payment/discovery")public Object discovery(){List<String> services = discoveryClient.getServices();for (String element : services) {System.out.println(element);}List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");for (ServiceInstance element : instances) {System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t"+ element.getUri());}return this.discoveryClient;}
}
修改 cloud-provider-payment8001 和 cloud-provider-payment8002 的启动类,分别加上注解,如下:
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8081 {public static void main(String[] args) {SpringApplication.run(PaymentMain8081.class,args);}
}
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8082 {public static void main(String[] args) {SpringApplication.run(PaymentMain8082.class,args);}
}
重启所有的服务,然后输入 http://localhost:8001/payment/discovery ,进行测试,如下图:
2.6 Eureka 自我保护
2.6.1 故障现象
保护模式主要用于一组客户端和 Eureka Server 之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server 将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在 Eureka Server 的首页看到以下这段提示,则说明 Eureka 进入了保护模式:
2.6.2 导致原因
为了防止 EurekaClient 可以正常运行,但是与 EurekaServer 网络不通情况下,EurekaServer 不会立刻将 EurekaClient 服务剔除。
默认情况下,如果 EurekaServer 在一定时间内没有接收到某个微服务实例的心跳,EurekaServer 将会注销该实例(默认 90 秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与 EurekaServer 之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka 通过“自我保护模式”来解决这个问题——当 EurekaServer 节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
在自我保护模式中,Eureka Server 会保护服务注册表中的信息,不再注销任何服务实例。它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着。
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让 Eureka 集群更加的健壮、稳定。
2.6.3 禁止自我保护
首先自我保护机制是默认开启的,可以通过配置来关闭这个机制,修改 cloud-eureka-server7001 的 application.yml 配置文件,如下:
eureka:instance:hostname: eureka7001.com # eureka服务端的实例名称client:# false 表示不向注册中心注册自己。register-with-eureka: false# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务fetch-registry: falseservice-url:# 设置与 Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。defaultZone: http://eureka7002.com:7002/eureka/server:# 关闭自我保护机制,保证不可用服务被及时踢除enable-self-preservation: false# 时间间隔调整为 2seviction-interval-timer-in-ms: 2000
cloud-eureka-server7002 也是如此配置,这里不再赘述。接下来修改 cloud-provider-payment8001 的 application.yml 配置文件,如下:
eureka:client:# 表示是否将自己注册进EurekaServer默认为true。register-with-eureka: true#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetchRegistry: trueservice-url:# defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版# 心跳检测与续约时间# 开发时设置小些,保证服务关闭后注册中心能即使剔除服务instance:# 设置名称instance-id: payment8001# 访问路径可以显示IP地址prefer-ip-address: true# Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)lease-renewal-interval-in-seconds: 1# Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务lease-expiration-duration-in-seconds: 2
cloud-provider-payment8002 也是如此配置,这里不再赘述。
2.6.4 测试
分别重启各个服务,然后手动停止 cloud-provider-payment8001 服务,如下图:
三、Zookeeper 服务注册与发现
3.1 Eureka 停止更新
自从 2018 年后,Eureka 就停止了更新,如下图:
3.2 Zookeeper 代替 Eureka
3.2.1 zookeeper 简介
zookeeper 是一个分布式协调工具,可以实现注册中心功能。可以使用 zookeeper 服务器取代 Eureka 服务器,zk 作为服务注册中心。
使用的前提是需要在 linux 服务器上安装 zookeeper,具体按照的步骤,可以参考我的这篇文章,这里不再赘述,记得把 linux 服务器的防火墙先关掉。
3.2.2 创建服务提供者
创建 cloud-provider-payment8004 模块作为服务的提供者,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-provider-payment8004</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- SpringBoot整合Web组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><groupId>com.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>
application.yml 的内容如下所示:
# 8004表示注册到zookeeper服务器的支付服务提供者端口号
server:port: 8004
#服务别名----注册zookeeper到注册中心名称
spring:application:name: cloud-provider-paymentcloud:zookeeper:connect-string: 192.168.229.167:2181
主启动类的内容如下所示:
@SpringBootApplication
// 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class PaymentMain8004 {public static void main(String[] args){SpringApplication.run(PaymentMain8004.class,args);}}
创建一个 Controller 类,内容如下所示:
package com.springcloud.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;import java.util.UUID;@RestController
public class PaymentController
{@Value("${server.port}")private String serverPort;@RequestMapping(value = "/payment/zk")public String paymentzk(){return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString();}
}
启动工程进行测试,如下:
3.2.3 注意
此时方式创建的 zookeeper 节点为临时节点,当关闭微服务模块后,节点一会就自动消失了
3.2.4 创建服务消费者
创建 cloud-consumerzk-order80 模块作为服务的提供者,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-consumer-order80</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--eureka-client--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.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>
application.yml 内容如下所示:
server:port: 80spring:application:name: cloud-consumer-ordercloud:# 注册到zookeeper地址zookeeper:connect-string: 192.168.229.167:2181
主启动类的内容如下所示:
@SpringBootApplication
public class OrderZK80 {public static void main(String[] args){SpringApplication.run(OrderZK80.class,args);}
}
配置类的代码如下所示:
@Configuration
public class ApplicationContextConfig {@Bean// 使用@LoadBalanced注解赋予RestTemplate负载均衡的能力@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}
}
Controller 的代码如下所示:
@RestController
public class OrderZKController
{public static final String INVOKE_URL = "http://cloud-provider-payment";@Autowiredprivate RestTemplate restTemplate;@RequestMapping(value = "/consumer/payment/zk")public String paymentInfo(){String result = restTemplate.getForObject(INVOKE_URL+"/payment/zk", String.class);System.out.println("消费者调用支付服务(zookeeper)--->result:" + result);return result;}}
启动服务进行验证,可以在 zookeeper 看到注册的节点,如下图:
还可以进行接口调用,如下图:
四、Consul 服务注册与发现
4.1 Consul 简介
4.1.1 Consul 是什么
HashiCorp Consul 是一种服务网络解决方案,使团队能够管理服务之间以及跨本地和多云环境和运行时的安全网络连接。 Consul 提供服务发现、服务网格、流量管理和网络基础设施设备的自动更新。您可以在单个 Consul 部署中单独或一起使用这些功能。
4.1.2 Consul 用途
1、服务发现,提供 HTTP 和 DNS 两种发现方式。
2、健康监测,支持多种方式,HTTP、TCP、Docker、Shell 脚本定制化监控
3、KV 存储
4、支持多数据中心
5、可视化 Web 界面
4.1.3 Consul 下载
现在地址在这,选择适合的版本,如下图:
下载完成后解压,如下图,双击安装包即可运行
4.1.4 Consul 使用
进入 consul.exe 目录的 cmd 命令行模式下,输入 consul --version 即可查看版本信息,如下图:
输入 consul agent -dev 使用开发者模式启动,如下图:
启动成功后,在浏览器输入 http://localhost:8500 即可访问 Consul 的首页信息,如下图:
4.2 创建服务提供者
新建支付模块 cloud-providerconsul-payment8006,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-provider-payment8006</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><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>
application.yml 内容如下所示:
# consul服务端口号
server:port: 8006spring:application:name: consul-provider-payment####consul注册中心地址cloud:consul:host: localhostport: 8500discovery:#hostname: 127.0.0.1service-name: ${spring.application.name}
主启动类代码如下所示:
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006
{public static void main(String[] args){SpringApplication.run(PaymentMain8006.class,args);}
}
业务类 Controller 代码如下所示:
@RestController
public class PaymentController {@Value("${server.port}")private String serverPort;@GetMapping("/payment/consul")public String paymentInfo(){return "springcloud with consul: "+serverPort+"\t\t"+ UUID.randomUUID().toString();}
}
启动工程,打开 Consul 的管理界面,可以看到,微服务注册成功了,如下图:
4.3 创建服务消费者
新建支付模块 cloud-consumerconsul-order80,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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.springcloud</groupId><artifactId>SpringCloud</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-consumerconsul-order80</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><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>
application.yml 内容如下所示:
# consul服务端口号
server:port: 80spring:application:name: cloud-consumer-order####consul注册中心地址cloud:consul:host: localhostport: 8500discovery:#hostname: 127.0.0.1service-name: ${spring.application.name}
主启动类代码如下所示:
@SpringBootApplication
// 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class OrderConsulMain80
{public static void main(String[] args){SpringApplication.run(OrderConsulMain80.class,args);}
}
业务类 Controller 代码如下所示:
@RestController
public class OrderConsulController
{public static final String INVOKE_URL = "http://consul-provider-payment"; @Autowiredprivate RestTemplate restTemplate;@GetMapping(value = "/consumer/payment/consul")public String paymentInfo(){String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul", String.class);System.out.println("消费者调用支付服务(consule)--->result:" + result);return result;}
}
配置类代码如下:
@Configuration
public class ApplicationContextConfig {@Bean//使用@LoadBalanced注解赋予RestTemplate负载均衡的能力@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}
}
启动工程,打开 Consul 的管理界面,可以看到,微服务注册成功了,如下图:
输入 http://localhost/consumer/payment/consul,进行测试,如下图:
五、三个注册中心对比
5.1 CAP 简介
C:Consistency 的缩写,表示强一致性
A:Availability 的缩写,表示可用性
P:Partition tolerance 的缩写,表示分区容错性
5.2 经典 CAP 图
CAP 理论关注粒度是数据,而不是整体系统设计的策略。
CAP 理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类,如下,其实在实际的环境中最多只能同时较好的满足两个。
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些
5.3 AP 架构(Eureka)
当网络分区出现后,为了保证可用性,系统 B 可以返回旧值,保证系统的可用性。违背了一致性 C 的要求,只满足可用性和分区容错,即 AP,如下图:
5.4 CP 架构(Zookeeper/Consul)
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性,违背了可用性 A 的要求,只满足一致性和分区容错,即 CP,如下图:
5.5 三个注册中心比较
组件名 | 语言 | CAP | 健康服务检查 | 对外暴露接口 | SpringCloud 集成 |
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |