Eureka服务注册与发现组件使用教程
- 服务注册与发现
- 负载均衡
- 远程调用
父模块的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>
<groupId>anyi.space</groupId>
<artifactId>distributedSystemLearn</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>member-service-provider_10000</module>
<module>member-service-consumer</module>
<module>common</module>
<module>eureka-server_9001</module>
</modules>
<packaging>pom</packaging>
<properties>
<!--指定各种版本的依赖-->
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring.boot.version>2.7.17</spring.boot.version>
<mysql.connector.version>8.3.0</mysql.connector.version>
<log4j.version>1.2.17</log4j.version>
<mybatis.plus.version>3.4.3.4</mybatis.plus.version>
<druid.version>1.2.16</druid.version>
<spring.colud.version>Hoxton.SR9</spring.colud.version>
</properties>
<dependencyManagement>
<dependencies>
<!--springBoot父项目的依赖,以pom配置文件的形式导入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springCloud父项目的依赖,以pom配置文件的形式导入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.colud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Eureka服务注册中心搭建
- Eureka依赖导入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- application.yaml配置文件的编写
- 单机服务注册中心不需要向服务注册中注册自己,也不需要向服务注册中心拉取服务信息(集群部署时需要)
server:
port: 9001
spring:
application:
name: eureka-server
eureka:
instance:
#Eureka服务端的实例名称
hostname: localhost
client:
#配置不向服务中注册自己,默认是true
register-with-eureka: false
#配置不向服务中抓取注册信息,因为自己就是服务端,默认是true
fetch-registry: false
#配置服务注册中心的地址
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
- 主启动类EurekaServerApplication.java的编写
- 使用"@EnableEurekaServer"注解标注该程序是Eureka的服务注册中心
package anyi.space.eurekaServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @ProjectName: distributedSystemLearn
* @FileName: EurekaServerApplication
* @Author: 杨逸
* @Data:2025/4/14 19:11
* @Description:
*/
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
-
测试
- 启动程序,打开浏览器访问'http://localhost:9001/',检验服务搭建成功
Eureka客户端搭建
服务提供客户端搭建
-
加入Erueka客户端依赖
<!--引入eureka-client依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
编写application.yaml配置文件
server: application: name: member-service-provider port: 10001 servlet: context-path: /member-provider spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/spring_cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl eureka: client: #是否将自己注册到Eureka Server中 register-with-eureka: true #是否从Eureka Server中获取注册信息,单节点不拉取也可以,集群部署时,需要拉取服务注册信息才能实现负载均衡 fetch-registry: true #Eureka Server的地址 service-url: defaultZone: http://localhost:9001/eureka/
-
编写主程序
- 使用"@EnableEurekaClient"注解表示改程序是Eureka的客户端
package anyi.space.memberServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @ProjectName: distributedSystemLearn * @FileName: MerberServiceApplication * @Author: 杨逸 * @Data:2025/4/13 18:53 * @Description: */ @EnableEurekaClient @SpringBootApplication(scanBasePackages = {"anyi.sapce.common","anyi.space.memberServer"}) public class MemberServiceApplication { public static void main(String[] args) { SpringApplication.run(MemberServiceApplication.class,args); } }
-
验证
- 打开浏览器访问'http://localhost:9001/'Euerka Server服务注册中心,检验Eureka Client是否注册到Euerka Server服务注册中心
服务调用客户端搭建
-
加入Eureka Client依赖
<!--引入eureka-client依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
编写application.yaml配置文件
server: port: 80 servlet: context-path: /member-consumer spring: application: name: member-consumer eureka: instance: #配置注册到Eureka Server中的名称 appname: member-consumer client: #是否将自己注册到Eureka Server中 register-with-eureka: true #是否从Eureka Server中获取注册信息,单节点不拉取也可以,集群部署时,需要拉取服务注册信息才能实现负载均衡 fetch-registry: true #Eureka Server的地址 service-url: defaultZone: http://localhost:9001/eureka/
-
编写主启动类
package anyi.space.memberConsumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @ProjectName: distributedSystemLearn * @FileName: MemberConsumerApplication * @Author: 杨逸 * @Data:2025/4/13 20:05 * @Description: */ @EnableEurekaClient @SpringBootApplication(exclude= DataSourceAutoConfiguration.class) public class MemberConsumerApplication { public static void main(String[] args) { SpringApplication.run(MemberConsumerApplication.class,args); } }
-
验证
Eureka自我保护机制
- 在默认情况下,Eureka启动了自我保护模式
- 默认情况下EurekaClient定时向EurekaServer端发送心跳包
- 如果Eureka在server端在一定时间内(默认90秒)没有收到EurekaClient发送心跳包,便会直接从服务注册列表中剔除该服务
- 如果Eureka开启了自我保护模式/机制,那么在短时间(90秒中)内丢失了大量的服务实例心跳,这时候EurekaServer会开启自我保护机制,不会剔除该服务(该现象可能出现在如果网络不通或者阻塞)因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问题而产生的
- 自我保护是属于CAP里面的AP分支,保证高可用和分区容错性
- 自我保护模式是—种应对网络异常的安全保护措施
- 它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。
- 使用自我保护模式,可以让Eureka集群更加的健壮、稳定
禁用自我保护模式(生产环境一般不禁用)
- 修改Eureka Server的application.yaml配置文件
- 禁用自我保护机制
- 修改心跳机制间隔超时时间
eureka:
server:
#关闭自我保护机制
enable-self-preservation: false
#心跳机制时间间隔(单位毫秒,默认是90*1000),心跳超时90秒后服务端会剔除超时的客户端
eviction-interval-timer-in-ms: 2000
- 修改Eureka Client的application.yaml
- 修改发送心跳的间隔时间
- 修改超时剔除服务的时间上限
eureka:
instance:
#心跳检测和续约的间隔时间
#Eureka Client向Eureka Server发送心跳信息的间隔时间
lease-renewal-interval-in-seconds: 30
#Eureka Server在收到最后一次心跳后等待的时间上限,超时将剔除该服务实例,默认是90秒
lease-expiration-duration-in-seconds: 90
搭建Eureka Server集群
- 微服务RPC远程服务调用最核心的是实现高可用
- 如果注册中心只有1个,它出故障,会导致整个服务环境不可用
- 解决办法∶搭建Eureka注册中心集群,实现负载均衡+故障容错
在Erueka Server 9001模块的基础上,再搭建一个Eureka Server 9002模块,将两个服务注册相互注册实现服务注册中心集群的搭建
-
新模块,加入Srueka Server的依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
配置Erueka Server 9002的application.yaml配置文件
- 向Erueka Server 9001注册服务
server: port: 9002 spring: application: name: eureka-server eureka: instance: #Eureka服务端的实例名称 hostname: localhost client: #配置向服务中注册自己,搭建集群 register-with-eureka: true #配置向服务中抓取注册信息 fetch-registry: true #配置服务注册中心的地址 service-url: #向Eureka Server 9001相互注册 defaultZone: http://localhost:9001/eureka server: #设置自我保护机制,默认开启 enable-self-preservation: true #心跳机制时间间隔(单位毫秒,默认是30*1000) eviction-interval-timer-in-ms: 2000
- 配置Eureka Server 9001向Eureka Server 9002注册
server: port: 9001 spring: application: name: eureka-server eureka: instance: #Eureka服务端的实例名称 hostname: localhost client: #配置向服务中注册自己,搭建集群 register-with-eureka: true #配置向服务中抓取注册信息 fetch-registry: true #配置服务注册中心的地址 service-url: #向Eureka Server 9002相互注册 defaultZone: http://localhost:9002/eureka server: #设置自我保护机制,默认开启 enable-self-preservation: true #心跳机制时间间隔(单位毫秒,默认是30*1000) eviction-interval-timer-in-ms: 2000
-
编写主程序
package anyi.space.eurekaServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @ProjectName: distributedSystemLearn * @FileName: EurekaServer * @Author: 杨逸 * @Data:2025/4/15 20:10 * @Description: */ @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
-
启动两个Eureka Server,验证
-
9001
-
9002
-
搭建Eureka Client集群
搭建服务提供方集群
- 因为member-service-provider-10001和member-service-provider-10002作为一个集群提供服务,因此需要将spring.application.name进行统一
- 这样消费方可以通过统一的别名进行负载均衡调用
-
新模块member-service-provider_10002,引入Eureka Client依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
配置application.yaml配置文件
- 配置member-service-provider_10002模块向两个Eureka Server注册
server: port: 10002 servlet: context-path: /member-provider spring: application: name: member-service-provider datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/spring_cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl eureka: instance: #配置注册到Eureka Server中的名称 appname: member-service-provider #心跳检测和续约的间隔时间 #Eureka Client向Eureka Server发送心跳信息的间隔时间 lease-renewal-interval-in-seconds: 30 #Eureka Server在收到最后一次心跳后等待的时间上限,超时将剔除该服务实例,默认是90秒 lease-expiration-duration-in-seconds: 90 client: #是否将自己注册到Eureka Server中 register-with-eureka: true #是否从Eureka Server中获取注册信息,单节点不拉取也可以,集群部署时,需要拉取服务注册信息才能实现负载均衡 fetch-registry: true #Eureka Server的地址 service-url: #配置两个Eureka Server的地址,用逗号分割 defaultZone: http://localhost:9001/eureka/,http://localhost:9002/eureka/
- 配置member-service-provider_10001模块向两个Eureka Server注册
server: port: 10001 servlet: context-path: /member-provider spring: application: name: member-service-provider datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql://localhost:3306/spring_cloud?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: hsp driver-class-name: com.mysql.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl eureka: instance: #配置注册到Eureka Server中的名称 appname: member-service-provider #心跳检测和续约的间隔时间 #Eureka Client向Eureka Server发送心跳信息的间隔时间 lease-renewal-interval-in-seconds: 30 #Eureka Server在收到最后一次心跳后等待的时间上限,超时将剔除该服务实例,默认是90秒 lease-expiration-duration-in-seconds: 90 client: #是否将自己注册到Eureka Server中 register-with-eureka: true #是否从Eureka Server中获取注册信息,单节点不拉取也可以,集群部署时,需要拉取服务注册信息才能实现负载均衡 fetch-registry: true #Eureka Server的地址 service-url: #配置两个Eureka Server的地址,用逗号分割 defaultZone: http://localhost:9001/eureka/,http://localhost:9002/eureka/
-
编写主程序
package anyi.space.memberServer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @ProjectName: distributedSystemLearn * @FileName: MemberServiceApplication * @Author: 杨逸 * @Data:2025/4/15 20:29 * @Description: */ @EnableEurekaClient @SpringBootApplication public class MemberServiceApplication { public static void main(String[] args) { SpringApplication.run(MemberServiceApplication.class, args); } }
-
启动两个member-service-provider,验证
配置服务消费方使用服务提供方集群
-
修改application.yaml配置文件,配置可以从两个服务注册中心拉取服务信息
server: port: 80 servlet: context-path: /member-consumer spring: application: name: member-consumer eureka: instance: #配置注册到Eureka Server中的名称 appname: member-consumer client: #是否将自己注册到Eureka Server中 register-with-eureka: true #是否从Eureka Server中获取注册信息,单节点不拉取也可以,集群部署时,需要拉取服务注册信息才能实现负载均衡 fetch-registry: true #Eureka Server的地址 service-url: #配置两个Eureka Server的地址,用逗号分割 defaultZone: http://localhost:9001/eureka/,http://localhost:9002/eureka/
-
使用"@LoadBalanced"注解配置RestTemplate通过负载均衡的方式调用服务提供方的服务
@Configuration public class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
-
使用RestTemplate通过服务别名的方式调用服务提供方的服务
- 服务别名,注意使用小写的别名
private static final String MEMBER_SERVICE_URL = "http://member-service-provider";
- 调用服务
package anyi.space.memberConsumer.service.impl; import anyi.sapce.common.entity.Member; import anyi.sapce.common.entity.ResponseResult; import anyi.space.memberConsumer.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.Map; /** * @ProjectName: distributedSystemLearn * @FileName: MemberServiceIml * @Author: 杨逸 * @Data:2025/4/13 20:10 * @Description: */ @RequiredArgsConstructor @Service public class MemberServiceIml implements MemberService { private final RestTemplate restTemplate; private static final String MEMBER_SERVICE_URL = "http://member-service-provider"; @Override public Member getById(Long id) { /** * 通过RestTemplate调用远程服务 */ ResponseEntity<ResponseResult> resultResponseEntity = restTemplate.getForEntity(MEMBER_SERVICE_URL + "/member-provider/member?id="+id, ResponseResult.class); Object data = resultResponseEntity.getBody().getData(); //映射请求结果 Map map = (Map) data; Member member = new Member(); id = Long.valueOf(map.get("id") + ""); member.setId(id); member.setName((String) map.get("name")); member.setPwd((String) map.get("pwd")); member.setEmail((String) map.get("email")); member.setGender((int)map.get("gender")); return member; } @Override public boolean save(Member member) { //直接传递参数,是使用json的格式提交数据 ResponseEntity<ResponseResult> responseResultResponseEntity = restTemplate.postForEntity(MEMBER_SERVICE_URL + "/member-provider/member", member, ResponseResult.class); //HttpEntity<String> stringHttpEntity = new HttpEntity<>(); Object data = responseResultResponseEntity.getBody().getData(); boolean flag = (boolean) data; return flag; } }
验证负载均衡
-
修改服务提供方member-service-provider_10001和member-service-provider_10002的返回结果携带服务提供方的信息
- 服务提供方member-service-provider_10001在响应结果添加额外信息
responseResult.setMsg(responseResult.getMsg() + "服务提供方10001");
- 服务提供方member-service-provider_10002在响应结果添加额外信息
responseResult.setMsg(responseResult.getMsg() + "服务提供方10002");
-
使用服务消费方访问两次服务提供方,验证轮询的负载均衡
- 使用日志打印两次调用的结果
log.info("远程调用服务返回结果:{}",objectMapper.writeValueAsString(resultResponseEntity.getBody()));
获取注册到Eureka Server的服务信息
使用DiscoveryClient类获取注册到Eureka Server的服务信息
我们可以通过使用服务发现类DiscoveryClient获取需要的服务实例信息,然后动态拼接服务地址,再使用http协议进行调用
-
在服务消费方主程序使用"@EnableDiscoveryClient"注解启用服务发现功能,获取注册的服务信息
package anyi.space.memberConsumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @ProjectName: distributedSystemLearn * @FileName: MemberConsumerApplication * @Author: 杨逸 * @Data:2025/4/13 20:05 * @Description: */ @EnableDiscoveryClient @EnableEurekaClient @SpringBootApplication(exclude= DataSourceAutoConfiguration.class) public class MemberConsumerApplication { public static void main(String[] args) { SpringApplication.run(MemberConsumerApplication.class,args); } }
-
在Controller注入一个DiscoveryClient类,用于获取注册的服务信息
-
使用DiscoveryClient获取服务信息
package anyi.space.memberConsumer.controller; import anyi.sapce.common.entity.Member; import anyi.sapce.common.entity.ResponseResult; import anyi.space.memberConsumer.service.MemberService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.nio.charset.StandardCharsets; import java.util.List; /** * @ProjectName: distributedSystemLearn * @FileName: MemberConsumerController * @Author: 杨逸 * @Data:2025/4/13 20:07 * @Description: */ @Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/member") public class MemberConsumerController { private final MemberService memberService; private final DiscoveryClient discoveryClient; private final ObjectMapper objectMapper; @GetMapping public ResponseResult<Member> getMemberById(Long id){ Member member = memberService.getById(id); return ResponseResult.okResult(member); } @PostMapping public ResponseResult addMember(Member member){ String hex = DigestUtils.md5DigestAsHex(member.getPwd().getBytes(StandardCharsets.UTF_8)); member.setPwd(hex); boolean save = memberService.save(member); return ResponseResult.okResult(save); } @GetMapping("/discovery") public ResponseResult discovery(){ //获取所有注册的服务名称 List<String> services = discoveryClient.getServices(); for (int i = 0; i < services.size(); i++) { String service = services.get(i); log.info("服务名称:{}",service); //获取对应服务的实例信息列表 List<ServiceInstance> instances = discoveryClient.getInstances(service); for (int j = 0; j < instances.size(); j++) { //获取具体服务实例的信息,比如IP和端口等 ServiceInstance instance = instances.get(j); try { log.info("服务名称:{},服务实例信息:{}",service,objectMapper.writeValueAsString(instance)); } catch (JsonProcessingException e) { e.printStackTrace(); log.error("服务名称:{},服务实例信息:{}",service,e.getMessage()); } } } return ResponseResult.okResult(services); } }
-
验证结果