【深入理解SpringCloud微服务】浅析微服务注册中心Eureka与nacos,手写实现一个微服务注册中心

news2024/9/20 10:52:47

【深入理解SpringCloud微服务】浅析微服务注册中心Eureka与nacos,手写实现一个微服务注册中心

  • 注册中心
  • 手写实现一个注册中心
    • 服务端设计
    • 客户端设计

注册中心

注册中心是微服务体系里面非常重要的一个核心组件,它最重要的作用就是实现服务注册与发现

在过去还没有微服务和注册中心的时候,一个服务存在对另一个服务的调用关系时,需要在自己服务的配置文件里面配置对方的ip端口,当发生调用时,需要读取配置文件里面对方的ip端口,组装请求url,发送请求。这种方式非常的不灵活,当被调用的服务集群用机器上下线时,调用方不能动态感知,需要手动修改配置然后重启服务,并且服务一旦多起来,维护这些配置也是一项繁琐的工作,很容易出错。

在这里插入图片描述

为了解决这个问题,于是就有了微服务注册中心。

注册中心是部署在分布式或者微服务环境下的一个中间件服务,提供服务的注册与发现功能。当使用了注册中心之后,调用方被称为服务消费者,被调用方被称为服务提供者。服务提供者启动时往注册中心注册,注册信息包括自己的服务名称、服务实例id、ip地址和端口等,注册中心把这些信息维护到内存注册表。服务消费者启动时(饿汉式加载)或者在发生调用时(懒加载)想注册中心发起服务发现请求,从注册中心拉取服务提供者注册上来的注册信息,缓存到本地的服务列表中。这样,服务消费者通过查询服务列表就能得知要调用的服务提供者的ip端口,无需在配置文件中进行配置,并且当服务提供者对应的服务集群有服务实例上下线时,服务消费者可以通过定时轮询注册中心或者注册中心主动通知的方式动态感知。

在这里插入图片描述

注册中心会在内存中维护一个服务注册表,用于存储服务提供者注册上来的信息。比如用一个双层Map,外层key是服务名,内层key是服务实例id(同一服务的不同实例组成集群,因此需要一个类似于id的唯一标识),value是ip端口。

在这里插入图片描述

内存中的注册信息有可能还会持久化或者存到外部的存储服务中,比如Mysql、redis、MongoDB、文件都可以。

在这里插入图片描述

注册中心为了避免单点故障,往往也是集群部署。因此,注册中心实例之间会有服务注册信息的同步。

在这里插入图片描述

当注册中心是集群式部署时,服务提供者启动时就通过某种方式选取到一台注册中心实例注册即可,注册中心会通过集群内同步把注册信息同步到其他注册中心实例。

在这里插入图片描述

由于服务有可能因为某些原因而出问题或者下线,服务注册中心需要通过某种方式对服务提供者进行健康检查,把不健康的服务实例从注册表中剔除。但是有的注册中心不会对服务提供者进行健康检查,而是给服务提供者注册上来的信息设置一个过期时间,服务提供者需要定期的进行服务续约,如果超过指定时间不续约,服务提供者的注册信息将会被注册中心从注册表中剔除。注册中心的注册表发生表动,会通知服务消费者,或者由服务消费者自己轮询感知注册表的变化。

在这里插入图片描述

手写实现一个注册中心

我们对注册中心已经有了一个认识,总结下来就是有一个服务注册服务端,维护了一个内存注册表,客户端请求服务端进行服务注册与发现,实际上就是读写内存注册表。然后注册中心服务端还要实现注册信息在集群内的同步、服务变更通知客户端、服务健康检查等功能。

那么,我们也可以实现一个自己的注册中心了。

服务端设计

我们还是参考Eureka和1.x版本的nacos,采用http服务端的实现方式。我们定义一个自己的Controller,名字就叫RegistryCenterController,是一个SpringMVC的Controller,接收客户端发来的http请求。

然后我们定义一个Service,名字叫RegistryCenterService,由它来处理内存注册表的读写,内存注册表就直接放在RegistryCenterService中。RegistryCenterController接收到请求之后,会调用registryCenterService进行请求处理。

在定义内存注册表的结构前,我们要定义一个用于存放注册信息的对象,我们定义一个MicroService对象用于封装服务提供者的注册信息,比如ip地址端口号等。

然后内存注册表的结构还是使用双层ConcurrentHashMap,外层key就是服务名serviceName(我们这里不考虑什么namespace和cluster之类的东西),内存key就是服务实例id,value就是MicroService,这样就是一个非常简单的双层ConcurrentHashMap结构的内存注册表,我们给它命名为registryTable。

客户端通过发送http请求来进行服务发现和服务注册,服务端通过RegistryCenterController接收http请求并调用RegistryCenterService读写内存注册表registryTable。

在这里插入图片描述

RegistryCenterController:

/**
 * @author huangjunyi
 * @date 2023/11/30 10:23
 * @desc
 */
@RestController
@RequestMapping("/registry/center")
public class RegistryCenterController {

    @Autowired
    private RegistryCenterService registryCenterService;

    ...

}

RegistryCenterService :

/**
 * @author huangjunyi
 * @date 2023/11/30 10:40
 * @desc
 */
@Service
public class RegistryCenterService {

	// 内存注册表,双层ConcurrentHashMap:[serviceName, [id, 服务实例信息]]
    private Map<String, Map<String, MicroService>> registryTable = new ConcurrentHashMap<>();

    ...
}

MicroService :

/**
 * @author huangjunyi
 * @date 2023/11/30 10:41
 * @desc
 */
public class MicroService implements Serializable {

    private String serviceName;

    private String id;

    private String ip;

    private int port;

    private long lastTime;

	...
}    

至于服务同步,我们也是做异步处理。在RegistryCenterService 内部定义一个LinkedBlockingQueue类型的变量作为队列,把注册上来的信息放到这个队列里面,就把MicroService对象放进去。然后使用一个后台线程去轮询这个队列,把MicroService同步到集群中的其他注册中心实例。

在这里插入图片描述
RegistryCenterService :

@Service
public class RegistryCenterService {

	// 内存注册表,双层ConcurrentHashMap:[serviceName, [id, 服务实例信息]]
    private Map<String, Map<String, MicroService>> registryTable = new ConcurrentHashMap<>();

	// 集群同步队列
    private LinkedBlockingQueue<MicroService> syncQueue = new LinkedBlockingQueue<>();

	...

    public boolean registry(MicroService microService) throws InterruptedException {
        ...
        // 注册上来的服务实例放入集群同步队列
        syncQueue.put(microService);
        return true;
    }

	...

    public void syncService() throws InterruptedException {
        ...
        // 循环取出队列中的服务实例
        while (syncQueue.peek() != null) {
            MicroService service = syncQueue.take();
            // 轮询每个集群节点
            for (String node : nodes) {
                ...
                // 通过http请求把实例信息同步过去
                Result result = restTemplate.postForObject(String.format("http://%s:%d/registry/center/sync", ip, port), service, Result.class);
                ...
            }
        }
    }
}

RegistryCenterController通过“/sync”接口接收到注册信息同步后,会调用registryCenterService.addService(microService)方法:

    @PostMapping("/sync")
    public Result<?> sync(@RequestBody MicroService microService) {
        if (registryCenterService.addService(microService)) {
            return Result.ok(null);
        }
        return Result.error("sync failed");
    }

registryCenterService.addService(microService)方法把注册信息写入内存注册表:

    public boolean addService(MicroService microService) {
        registryTable.putIfAbsent(microService.getServiceName(), new ConcurrentHashMap<>());
        registryTable.get(microService.getServiceName()).put(microService.getId(), microService);
        LOGGER.info("this service[{}] added", microService.getServiceName() + ":" + microService.getId());
        return true;
    }

SyncServiceTask :

/**
 * @author huangjunyi
 * @date 2023/11/30 17:33
 * @desc
 */
@Component
public class SyncServiceTask {

    @Autowired
    private RegistryCenterService registryCenterService;

    @Scheduled(cron = "0/10 * * * * ?")
    public void syncService() throws InterruptedException {
    	// 定时任务调用registryCenterService的syncService方法进行集群同步
        registryCenterService.syncService();
    }

}

然后是健康检查,我们也是开一个后台线程,定时检查内存注册表中的服务实例信息里面的最近一次续约时间,超过30s没有续约,就把他踢掉,然后同步到集群中的其他注册中心。

在这里插入图片描述

/**
 * @author huangjunyi
 * @date 2023/11/30 11:43
 * @desc
 */
@Component
public class HealthCheckTask {

    @Autowired
    private RegistryCenterService registryCenterService;

    @Scheduled(cron = "0/30 * * * * ?")
    public void healthCheck() {
    	// 定时任务调用registryCenterService的healthCheck方法进行监控检查
        registryCenterService.healthCheck();
    }

}

最后,我们接入SpringBoot提供的自动装配机制,完成我们注册中心的自动配置,spring.factories文件配置指定我的配置类RegistryCenterServerConfig,然后我们的配置类RegistryCenterServerConfig通过@ComponentScan注解扫描RegistryCenterController、RegistryCenterService、定时任务类等一些核心类,定时任务使用Spring的@EnableScheduling和@Scheduled注解。

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.huangjunyi1993.simple.microservice.registry.center.server.config.RegistryCenterServerConfig

RegistryCenterServerConfig

@Configuration
@EnableScheduling
@ComponentScan(basePackages = {
        "com.huangjunyi1993.simple.microservice.registry.center.server.controller",
        "com.huangjunyi1993.simple.microservice.registry.center.server.service",
        "com.huangjunyi1993.simple.microservice.registry.center.server.task"})
@EnableConfigurationProperties({NodesProperties.class})
public class RegistryCenterServerConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

客户端设计

客户端方面,我们定义一个RegistryCenterClient用于通过OkHttp请求服务端进行服务注册和发现,RegistryCenterClient实现ApplicationListener<ContextRefreshedEvent>接口,监听ContextRefreshedEvent事件触发服务注册和服务发现。然后通过Spring的@EnableScheduling和@Scheduled注解开启定时任务使用OkHttp定时发送心跳。

在这里插入图片描述

/**
 * @author huangjunyi
 * @date 2023/11/30 20:21
 * @desc
 */
public class SimpleRegistryCenterClient implements RegistryCenterClient, EnvironmentAware, ApplicationListener<ContextRefreshedEvent> {

	...

	@Override
    public boolean sendHeartbeatToServer() {
        // 发送心跳,就是使用OkHttp请求注册中心服务端...
    }

	// 服务注册和服务发现
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 解析注册中心集群各实例的ip地址和端口
        String registryCenterServers = registryCenterConfig.getServers();
        String[] registryCenterServerArr = registryCenterServers.split(",");
        registryCenterServerList = new ArrayList<Server>();
        for (String registryCenterServer : registryCenterServerArr) {
            String[] ipPort = registryCenterServer.split(":");
            Server server = new Server();
            server.setIp(ipPort[0]);
            server.setPort(Integer.valueOf(ipPort[1]));
            registryCenterServerList.add(server);
        }
		// 服务注册
        if (microserviceConfig.isRegistry()) {
        	registryToServer(); 
        }
        
        // 服务发现
        if (!CollectionUtils.isEmpty(microserviceConfig.getSubscribeServiceNames())) {
        	for (String subscribeServiceName : microserviceConfig.getSubscribeServiceNames()) {
        		updateServiceList(subscribeServiceName);
        	}
        }
    }

    @Override
    public boolean registryToServer() {
		// 服务注册,就是使用OkHttp请求注册中心服务端...
	}

	// 服务发现
	public boolean updateServiceList(String serviceName) {
        serviceListMap.putIfAbsent(serviceName, new CopyOnWriteArrayList<>());
        // 轮询每个注册中心服务端实例
        for (Server server : registryCenterServerList) {
            // OKHttp代码....
            try {
                response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                	// 将拉取到的注册信息缓存到serviceListMap中
                    Result result = JSONObject.parseObject(Objects.requireNonNull(response.body()).string(), Result.class);
                    List<MicroService> microServiceList = JSONObject.parseArray(JSONObject.toJSONString(result.getData()), MicroService.class);
                    if (CollectionUtils.isEmpty(microServiceList)) {
                    	LOGGER.warn("serviceList is Empty, serviceName={}", serviceName);
                    	continue;
                    }
                    serviceListMap.get(serviceName).clear();
                    serviceListMap.get(serviceName).addAll(microServiceList);
                    return true;
                }
                LOGGER.warn("updateServiceList {} failed: {}", server, response.body() != null ? response.body().toString() : "");
            } catch (IOException e) {
                LOGGER.error("updateServiceList {} error ", server, e);
            }

        }

        return false;
    }
	...
}

客户端的三个定时任务,代码就不看了:
在这里插入图片描述

RegistryCenterClient也是通过SpringBoot的自动装配机制自动创建。

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.huangjunyi1993.simple.microservice.registry.center.client.config.RegistryCenterClientConfig

RegistryCenterClientConfig

@Configuration
@EnableScheduling
@ComponentScan(basePackages = {"com.huangjunyi1993.simple.microservice.registry.center.client.task"})
@EnableConfigurationProperties({MicroserviceConfig.class, RegistryCenterConfig.class})
public class RegistryCenterClientConfig {

    @Bean
    @ConditionalOnMissingBean(RegistryCenterClient.class)
    public RegistryCenterClient registryCenterClient() {
        return new SimpleRegistryCenterClient();
    }

}

详细代码可以从git上下载:https://gitee.com/huang_junyi/simple-microservice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1939252.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode 远程 Ubuntu 系统

1、在 Ubuntu 下检查 sshd 守护进程是否开启 ps -aux | grep sshd如果没有开启&#xff0c;请在 Ubuntu 下输入指令安装 sudo apt-get install openssh-server2、首先打开 Windows 下的 vscode&#xff0c;点击左下角图标打开远程窗口 3、打开远程窗口&#xff0c;选择“Con…

谷粒商城实战笔记-38-前端基础-Vue-指令-单向绑定双向绑定

文章目录 一&#xff0c;插值表达式注意事项1&#xff1a;不适合复杂的逻辑处理注意事项2&#xff1a;插值表达式支持文本拼接注意事项3&#xff1a;插值表达式只能在标签体中 二&#xff0c;v-html和v-textv-textv-html区别总结&#xff1a;最佳实践 三&#xff0c;v-model复选…

论文阅读:Speculative RAG: Enhancing Retrieval Augmented Generation through Drafting

论文地址&#xff1a;https://arxiv.org/abs/2407.08223 RAG 将 LLM 的生成能力与外部知识源相结合&#xff0c;以提供更准确和最新的响应。最近的 RAG 进展侧重于通过迭代 LLM 完善或通过 LLM 的额外指令调整获得自我批判能力来改进检索结果。在这项工作中&#xff0c;作者介…

MySQL:JOIN 多表查询

多表查询 在关系型数据库中&#xff0c;表与表之间是有联系的&#xff0c;它们通过 外键 联系在一起&#xff0c;所以在实际应用中&#xff0c;经常使用多表查询。多表查询就是同时查询两个或两个以上的表。 MySQL多表查询是数据库操作中非常重要的一部分&#xff0c;它允许你…

《从C/C++到Java入门指南》- 17.命令行参数

命令行参数 一直写代码的童鞋可能留意到了&#xff0c;main函数中会传入一个String args[]的字符串数组。 这个数组由JVM接收用户输入并传给main函数。 import java.util.*; public class Main {public static void main(String[] args) {for (String arg : args) {System.out…

[Redis]典型应用——缓存

什么是缓存 缓存&#xff08;Cache&#xff09;是一种用于临时存储数据的机制&#xff0c;目的是提高数据访问速度和系统性能。 核心思路就是把一些常用的数据放到触手可及(访问速度更快)的地方&#xff0c;方便随时读取 缓存是一个相对的概念&#xff0c;比如说&#xff0c…

[CSS] 浮动布局的深入理解与应用

文章目录 浮动的简介元素浮动后的特点解决浮动产生的影响浮动后的影响解决浮动产生的影响 浮动相关属性实际应用示例示例1&#xff1a;图片与文字环绕示例2&#xff1a;多列布局示例3&#xff1a;响应式布局 总结 浮动布局是CSS中一种非常强大的布局方式&#xff0c;最初设计用…

as是python关键字吗

关键字as的作用把紧跟其后的对象代替其前方的一个对象&#xff0c;其作用效果如下所示&#xff1a; import scrapy as tools 这个表达式中就是在当前这个模块中&#xff0c;使用tools可以代替scrapy&#xff0c;相当于C中的宏定义。在该as作用域中&#xff0c;可以使用tools来代…

STM32CubeIDE工程编译提示“has a LOAD segment with RWX permissions”解决办法

0 问题描述 使用STM32CubeIDE进行STM32MP135的开发时&#xff0c;编译过程打印了如下警告&#xff1a; 描述&#xff1a;has a LOAD segment with RWX permissions1 解决办法 右键工程&#xff0c;依次点击Properties->C/C Build->Settings->MCU GCC Linker->M…

如何将PDF转换成可以直接编辑的CAD图纸?

PDF图纸是为了让用户更好的阅览CAD文件&#xff0c;但是&#xff0c;当我们想要对其进行编辑的时候&#xff0c;PDF图纸就是一个麻烦了。那么PDF转换成CAD后可以编辑吗&#xff1f;如何将PDF转换成可以直接编辑的CAD图纸呢&#xff1f;本篇给你答案。 1、启动迅捷CAD编辑器&…

vcs+verdi搭建基础仿真的版本V1

2024-7-20 数字软件安装&#xff0c;仿真环境测试 dut重新修改 makefile重新修改 verdi整合完成 dut.v module dut ( );reg clk;initial beginclk 0;forever begin#10 clk ~clk;endendinitial begin$fsdbDumpfile("verilog.fsdb");$fsdbDumpvars();$vcdpluson;$…

WAAP安全防护能力

利用传统安全漏洞发动攻击的难度不断提升&#xff0c;攻击者的重心从传统的应用漏洞转向无明显攻击特征&#xff0c;模拟合法业务操作的自动化攻击。Web应用程序和API面临众多攻击场景&#xff0c;根据国内的情况进行汇总分析&#xff0c;主要分为恶意爬虫防护、安全攻击防护、…

电脑显示mfc140u.dll丢失的修复方法,总结7种有效的方法

mfc140u.dll是什么&#xff1f;为什么电脑会出现mfc140u.dll丢失&#xff1f;那么mfc140u.dll丢失会给电脑带来什么影响&#xff1f;mfc140u.dll丢失怎么办&#xff1f;今天详细给大家一一探讨一下mfc140u.dll文件与mfc140u.dll丢失的多种不同解决方法分享&#xff01; 一、mfc…

【Espressif-ESP32S3】【VScode】安装【ESP-IDF】插件及相关工具链

一、ESP-IDF简介 二、VScode安装ESP-IDF插件 三、安装ESP-IDF、ESP-IDF-Tools以及相关工具链 四、测试例程&编译烧录 五、IDF常用指令 资料下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/15Q2rl2jpIaKfj5rATkYE6g?pwdGLNG 提取码&#xff1a;GLNG 一、ESP-…

Linux 文件安装的mysql 启动

1、找到my.cnf 2、确定文件类容&#xff1a; 并确保这些重要的配置&#xff1a;basedir 、datadir、socket 文件或目录都存在 3、找到mysqld 位置 4、启动mysqld mysqld --defaults-file/etc/my.cnf --usermysql

Codeforces Round 960 (Div. 2)

文章目录 题目链接A. Submission BaitB. Array CraftC. Mad MAD Sum 题目链接 点击跳转codeforces A. Submission Bait 这是一道博弈题&#xff0c;比较简单&#xff0c;但是赛时直接就WA了两发&#xff0c;刚开始把题想简单了&#xff0c;第二发没有考虑清楚。  题意&#…

【云原生】Prometheus 服务自动发现使用详解

目录 一、前言 二、Prometheus常规服务监控使用现状​​​​​​​ 2.1 Prometheus监控架构图 2.2 Prometheus服务自动发现的解决方案 三、Prometheus服务自动发现介绍 3.1 什么是Prometheus服务自动发现 3.2 Prometheus自动服务发现策略 3.3 Prometheus自动服务发现应用…

又缩水Unity7月闪促限时4折活动模块化角色模板编辑器场景美术插件拖尾怪物3D模型UI载具AI对话TPS飞机RPG和FPS202407

Flash Deals are Coming Back! 限时抢购又回来了&#xff01; July 17, 2024 8:00:00 PT to July 24, 2024 7:59:00 PT 太平洋时间 2024 年 7 月 17 日 8&#xff1a;00&#xff1a;00 至 2024 年 7 月 24 日 7&#xff1a;59&#xff1a;00&#xff08;太平洋时间&#xff09;…

ThreadX打印tick(时钟滴答)方法

0 工具准备 ThreadX V6.2.0 1 ThreadX打印tick&#xff08;时钟滴答&#xff09;方法 int ostick(void) {printf("tick : %lu\r\n", _tx_time_get());return 0; }打印效果展示&#xff1a;

240717.LeetCode——2974.最小数字游戏

题目描述 你有一个下标从 0 开始、长度为 偶数 的整数数组 nums &#xff0c;同时还有一个空数组 arr 。Alice 和 Bob 决定玩一个游戏&#xff0c;游戏中每一轮 Alice 和 Bob 都会各自执行一次操作。游戏规则如下&#xff1a; 每一轮&#xff0c;Alice 先从 nums 中移除一个 …