Nacos-手写注册中心基本原理

本文已收录于专栏
《中间件合集》

概念说明

  注册中心是微服务架构中的纽带,类似于“通讯录”,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址并进行调用。注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的,更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。

需求分析

  我们在项目中使用nacos的时候只要进入一个nacos对应的依赖就可以了,不需要额外添加其他的代码,对于项目已启动关于客户端的信息注册到服务端,以及注册列表发生变化的时候拉取最新的注册列表等相关功能都不需要我们自己来写。我们只需要在pom文件中引入nacos的依赖这些功能就都有了。

  1. 需要有服务端(nacos)对客户端注册的信息进行储存和管理
  2. 需要有客户端向服务器注册自己信息包括IP地址端口号等内容
  3. 需要有服务端提供的SDK,用来项目启动注册信息和拉取最新注册列表等用能的
    在这里插入图片描述

核心功能

  1. 「 服务注册与发现 」:Nacos可以作为服务注册中心,服务提供者可以将自己的服务注册到Nacos中,而服务消费者可以从Nacos中发现并获取可用的服务实例。Nacos支持主流的服务注册与发现协议,包括基于HTTP的RESTful接口和基于DNS的服务发现。
  2. 「 动态配置管理 」:Nacos提供了一个统一的配置管理平台,可以集中管理应用程序的配置信息。它支持动态配置更新和推送,当配置发生变化时,Nacos会及时通知到应用程序。Nacos还支持配置的版本管理和灰度发布,可以方便地进行配置的管理和控制。
  3. 「 服务健康监测 」:Nacos可以对注册到其上的服务进行健康检查,通过定时发送心跳来判断服务的可用性。当服务不可用时,Nacos会将其从服务列表中移除,从而保证服务的高可用性和可靠性。
  4. 「 负载均衡 」:Nacos提供了负载均衡的能力,可以根据不同的负载均衡策略将请求分发到不同的服务实例上,从而实现请求的负载均衡。
  5. 「 服务路由与流量管理 」:Nacos支持服务的动态路由和流量管理,可以根据不同的规则将请求导向不同的服务实例,从而实现服务的灵活路由和流量控制。
  6. 「 配置共享与分组管理 」:Nacos支持配置的共享和分组管理,可以将配置按照不同的分组进行管理和控制,实现不同环境下的配置隔离和共享。

代码实现

AService模块

业务部分只需要从nacos中获取IP地址请求其他服务器即可。

import com.example.client.Controller.SDKController;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @BelongsProject: ServiceA
 * @BelongsPackage: com.example.servicea.Controller
 * @Author: Wuzilong
 * @Description: A服务
 * @CreateTime: 2023-06-06 18:43
 * @Version: 1.0
 */
@RestController
@RequestMapping("/A")
public class ServiceAController {

    @Autowired
    SDKController sdkController;
    @GetMapping("/getServiceIp")
    public void getServiceIp() throws JsonProcessingException {
            String serviceIp = sdkController.random("B");
            String url = "http://"+serviceIp+"/B/receiveMessage";
            RestTemplate restTemplate=new RestTemplateBuilder().build();
            ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
            if (forEntity.getStatusCode() == HttpStatus.OK) {
                System.out.println("调用B服务成功!IP地址为"+serviceIp);
            }
    }

}

配置文件中需要配置连接nacos服务的地址以及本服务的信息

server:
  port: 9001
  name: A
  url: localhost:9000
  key: ip

在pom文件中引入我们自己封装的NacosSDK服务

        <dependency>
            <groupId>com.example</groupId>
            <artifactId>SDK</artifactId>
            <version>2.5-20230615.123611-1</version>
        </dependency>

BService模块

业务部分只需要编写响应A服务调用B服务的接口即可,说明调用成功

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @BelongsProject: ServiceB
 * @BelongsPackage: com.example.serviceb.Controller
 * @Author: Wuzilong
 * @Description: B服务
 * @CreateTime: 2023-06-07 19:08
 * @Version: 1.0
 */
@RestController
@RequestMapping("/B")
public class ServiceBController {


    @GetMapping("/receiveMessage")
    public void receiveMessage(){
        System.out.println("B:我被调用了");
    }
}

配置文件中需要配置连接nacos服务的地址以及本服务的信息

server:
  port: 9002
  name: B
  url: localhost:9000
  key: ip

在pom文件中引入我们自己封装的NacosSDK服务

        <dependency>
            <groupId>com.example</groupId>
            <artifactId>SDK</artifactId>
            <version>2.5-20230615.123611-1</version>
        </dependency>

B服务可以启动多个为了验证负载均衡。当有高并发请求的时候我们可以把请求的压力分配到每一个B服务上,减少只有一个B服务的压力。还有就是当一个B服务不能正常访问的时候我们访问其他的B服务。

NacosService模块

nacos服务端主要的服务:更新注册列表、通知各客户端拉取最新注册列表、提供最新的注册列表

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @BelongsProject: Serve
 * @BelongsPackage: com.example.controller
 * @Author: Wuzilong
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-06-05 20:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("/nacosServe")
public class ConfigCenterController {


        Map<String,Map<String,String>> registerCenter =new HashMap<>();


        /**
        * @Author:Wuzilong
        * @Description: 将主机的信息注册进来并通知sdk更新注册列表
        * @CreateTime: 2023/6/9 10:25
        * @param:  主机信息
        * @return:  void
        **/
        @PostMapping(value = {"/setRegisterContext"})
        public void setRegisterContext( @RequestBody Map<String, Map<String,String>> registerContext) throws Exception {

                registerCenter.putAll(registerContext);
                System.out.println(registerContext.keySet().toString().replaceAll("\\[|\\]", "")+"服务,注册成功"+"注册的内容是"+registerCenter);

                if(registerCenter.size()>1){
                        for (Map.Entry<String, Map<String,String>> entry:registerCenter.entrySet()){
                                for (Map.Entry<String,String> entry1:entry.getValue().entrySet()){
                                        // 发送POST请求
                                        String url = "http://"+entry1.getValue()+"/"+"/configClientServe"+"/getRegisterContext";
                                        RestTemplate restTemplate=new RestTemplateBuilder().build();
                                        ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
                                        // 处理响应
                                        if (forEntity.getStatusCode() == HttpStatus.OK) {
                                                System.out.println("注册列表更新了,通知了"+entry.getKey()+"服务的SDK");
                                        }
                                }

                        }
                }
        }



        //返回更新后的注册列表
        @GetMapping(value = {"/getRegisterContext"})
        public Map<String,Map<String,String>> getRegisterContext(){
                return registerCenter;
        }

}

NacosSDK模块

SDK是我们自己封装的用来让其他客户端集成使用的,其中包括了:项目启动把客户端注册到注册列表中、接收到注册列表更新的消息拉取最新的注册列表、负载均衡的两种策略(轮询和随机)

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.net.InetAddress;
import java.util.*;

/**
 * @BelongsProject: Client
 * @BelongsPackage: com.example.client.Controller
 * @Author: Wuzilong
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-06-06 19:40
 * @Version: 1.0
 */
@RestController
@RequestMapping("/configClientServe")
@PropertySource("classpath:application.yml")
public class SDKController  implements ApplicationRunner{

    public Map<String, Map<String,String>> registerCenter =new HashMap<>();
    

    int index = 0;

    @Value("${server.port}")
    private String serverPort;

    @Value("${server.name}")
    private String serverName;

    @Value("${server.url}")
    private String serverIp;

    @Value("${server.key}")
    private String serverKey;


    //获取server中的注册列表
    @GetMapping(value = {"/getRegisterContext"})
    public void getRegisterContext() throws JsonProcessingException {
        System.out.println("注册列表更新了,去拉取一份新的注册列表");
        String url = "http://"+serverIp+"/nacosServe/getRegisterContext";
        RestTemplate restTemplate=new RestTemplateBuilder().build();
        ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
        if (forEntity.getStatusCode() == HttpStatus.OK) {
            String body = forEntity.getBody();
            ObjectMapper objectMapper = new ObjectMapper();
            registerCenter = objectMapper.readValue(body, new TypeReference<>() {});
            System.out.println("新的注册列表拉取完毕,注册列表的内容为"+registerCenter);

        }
    }


    //项目启动后把本服务的信息注册到nacosServe上
    @Override
    public void run(ApplicationArguments args) throws Exception {

        String url = "http://"+serverIp+"/nacosServe/setRegisterContext/";
        RestTemplate restTemplate=new RestTemplateBuilder().build();
        Map<String, Object> requestBody = new HashMap<>();
        Map<String,String> param=new HashMap<>();
        param.put(serverKey, InetAddress.getLocalHost().getHostAddress()+":"+serverPort);
        requestBody.put(serverName, param);
        HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody);
        restTemplate.postForEntity(url,request,String.class);
    }


    //轮询获取服务的IP地址
    public  String polling(String serverName){
        List<String> pollingList = this.getList(serverName);
        String ipContext = pollingList.get(index);
        index=(index+1)%pollingList.size();
        return ipContext;
    }


    //随机获取可用的IP地址
    public String random(String serverName){
        List<String> randomList = this.getList(serverName);
        Random random =new Random();
        int randomIndex = random.nextInt(randomList.size());
       return randomList.get(randomIndex);
    }



    //获取客户端想要请求服务的可用IP地址
    public List<String> getList(String serverName){
        List<String> list=new ArrayList<>();
        for (Map.Entry<String, Map<String,String>> entry:registerCenter.entrySet()){
            if(entry.getKey().contains(serverName)){
                for (Map.Entry<String,String> entry1:entry.getValue().entrySet()){
                    list.add(entry1.getValue());
                }
            }
        }
        return  list;
    }
}

注意事项

  • 找到一个项目一启动就会触发的操作,保证只要启动就会将此服务的信息同步放到注册表中。
  • 想到要写一个SDK服务,将其打成jar包,放到各个业务服务中,通过嵌入的方式和读取配置文件的方式,实现服务之间的调用
  • 使用注解,可以获取配置文件中的写的端口号等信息,可以进行灵活变更。
  • 使用restTemplate实现服务之间的调用
  • 引入SDK服务需要考虑启动类和业务类的路径,确保程序启动能够扫描到引入的业务类

总结提升

  1. 思想上移,行动下移:之前对nacos注册中心都是在概念,通过手动实现把nacos注册中心写出来之后对于nacos注册中心有了更深入的理解
  2. 知其然也要知其所以然:之气只是简单的使用,哪里出现了问题也不清楚只能靠蒙和猜来解决问题。现在可以非常明确的知道是哪个环节出现了问题。底层原理明确使用起来也非常的简单。
  3. 不断迭代完善:这个版本没有添加健康检测机制后面随着不断的版本迭代会非常其他相关的内容。

🎯 此文章对你有用的话记得留言+点赞+收藏哦🎯
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

武梓龙_Wzill

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值