RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到。

那么,Consumer 要从哪里获取 Provider 的地址呢?

能不能 Consumer 自己配置 Provider 的地址?

这种方式理论上是可行的,不过事实上没人这么做。这种方式有以下缺点:

  1. Consumer 每引用一个接口,需要配置一次 Provider 的服务地址,配置繁琐易错。
  2. Consumer 引用其他业务组的服务,需要跨团队沟通,沟通成本高。
  3. Provider 如果换服务器、挂掉、新增,都需要通知到 Consumer 去修改服务地址,配置修改可能不及时造成服务异常。
  4. Consumer 如果引用很多服务,那么配置会非常杂乱,管理起来非常麻烦。

从上面的缺点来看,最好的方式是找个地方把配置管理起来

例如,把配置放到统一的数据库中,Provider 启动的时候,把自己的地址和接口写到表中; Consumer 在请求接口之前,就可以从表里获取该接口对应的Provider地址。

其实,这种把配置统一管理的地方,就叫 注册中心

注册中心就像中间桥梁,连接ProviderConsumer。三方关系示意图如下:



注册中心 只是 Provider 感知 Consumer 的一种方式而已,最终 Provider 调用 Consumer 接口还是以直连的方式进行。

Provider 注册或者取消注册,注册中心会通知 Consumer,保证 Consumer 感知服务状态的及时性。

注册中心的特性

一个合格的注册中心,需要有以下的特性:

1. 存储

可以简单地将注册中心理解为一个存储系统,存储着服务与服务提供方的映射表。一般注册中心对存储没有太多特别的要求,甚至夸张一点,你可以基于数据库来实现一个注册中心。

2. 高可用

注册中心一旦挂掉,Consumer 将无法获取 Provider 的地址,整个微服务将无法运转。

当然 Consumer 可以添加本地缓存,从某种角度上看,是允许注册中心短暂挂掉的。

3. 健康检查

Provider 向注册中心注册服务之后,注册中心需要定时向 Provider 发起健康检查,当 Provider 宕机的时候,注册中心能更快发现 ,从而将宕机的 Provider 从注册表中移除。

这特性数据库、Redis 都不具有,因此他们不适合做注册中心。

4. 监听状态

当服务增加、减少 Provider 的时候,注册中心除了能及时更新,还要能主动通知 Consumer,以便 Consumer 能快速更新本地缓存,减少错误请求的次数。

这一特性同样数据库、Redis都不具有。

目前主流的注册中心有:ZookeeperEurekaNacosConsul 等。

由于本文主要是讲注册中心的实现,就不详细讲各种注册中心的差异、优缺点了,有兴趣的同学可以看这里

下面我们来讲 ccx-rpc 的注册中心是如何实现的。

注册中心的设计与实现

接口定义

下面是注册中心的接口,最简单就包含两个方法:注册查找

public interface Registry {

    /**
* 向注册中心注册服务
*
* @param url 注册者的信息
*/
void register(URL url); /**
* 查找注册的服务
*
* @param condition 查询条件
* @return 符合查询条件的所有注册者
*/
List<URL> lookup(URL condition);
}

本地缓存

为了减缓注册中心的压力,需要加上本地缓存,减少请求。同时也可以增加可用性,当注册中心挂的时候,本地还可以使用缓存中的数据。这部分逻辑否装在 AbstractRegistry 中,其他的实现都继承 AbstractRegistry

变量 registered 将服务信息缓存在 Map 中,服务名为 Key,Value 则是该服务注册的 Provider 列表。

/**
* 已注册的服务的本地缓存。{serviceName: [URL]}
*/
private final Map<String, Set<String>> registered = new ConcurrentHashMap<>();

当注册的 Provider 增加、减少的时候,会全量更新该服务下的 Provider 列表。

/**
* 重置。真实拿出注册信息,然后加到缓存中。
*/
public List<URL> reset(URL condition) {
// 获取服务名
String serviceName = getServiceNameFromUrl(condition);
// 将原来注册信息本地缓存删掉
registered.remove(serviceName);
// 重新从注册中心获取
List<URL> urls = doLookup(condition);
for (URL url : urls) {
// 将所有 Provider 添加到本地缓存
addToLocalCache(url);
}
return urls;
} /**
* 添加到本地缓存
*/
private void addToLocalCache(URL url) {
String serviceName = getServiceNameFromUrl(url);
if (!registered.containsKey(serviceName)) {
registered.put(serviceName, new ConcurrentHashSet<>());
}
registered.get(serviceName).add(url.toFullString());
}

Zookeeper 实现

ccx-rpc 中,注册中心实现了 zookeeper,实现类是 ZkRegistry

Zookeeper 客户端使用的是 Curator 框架,比官方的好用多了。

1. 注册

服务注册的时候,会在 /ccx-rpc/${serviceName}/providers 下创建一个临时节点

为什么是临时节点呢?临时节点有个功能就是,当客户端断开连接的时候,该客户端创建的节点都会自动删除,这个特性非常适合注册中心。

public void doRegister(URL url) {
zkClient.createEphemeralNode(toUrlPath(url));
watch(url);
}

创建的临时节点的内容是 Provider 的 URL 信息

示例:ccx-rpc://192.168.10.111:5525?interface=com.ccx.rpc.demo.service.api.UserService&version=

因为 URL 中包含 /,所以需要进行 url 编码,最终在 Zookeeper 存的是:

ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=

/**
* 转成全路径,包括节点内容。
* 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers/ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=
*/
private String toUrlPath(URL url) {
return toServicePath(url) + "/" + urlEncoder.encode(url.toFullString(), charset);
} /**
* 转成服务的路径。
* 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers
*/
private String toServicePath(URL url) {
return getServiceNameFromUrl(url) + "/" + RegistryConst.PROVIDERS_CATEGORY;
}

2. 查找

Consumer 直接获取服务路径下的所有子节点即可。

public List<URL> doLookup(URL condition) {
List<String> children = zkClient.getChildren(toServicePath(condition));
List<URL> urls = children.stream()
.map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
.collect(Collectors.toList());
return urls;
}

3. 监听

Zookeeper 还有一个很强的功能:监听。当监听的路径发生状态变化时,会全量更新(reset)对应的服务的本地缓存。reset 方法在上面的 AbstractRegistry 有讲到,这里就不重复贴代码了。

/**
* 监听
*/
private void watch(URL url) {
String path = toServicePath(url);
zkClient.addListener(path, (type, oldData, data) -> {
reset(url);
});
}

那么,我们是如何知道要监听哪些路径的呢?当 AbstractRegistry 本地缓存不存在的时候,会请求到 ZkRegistrydoLookup,请求出来的 Provider 都进行监听。

public List<URL> doLookup(URL condition) {
List<String> children = zkClient.getChildren(toServicePath(condition));
List<URL> urls = children.stream()
.map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
.collect(Collectors.toList());
// 获取到的每个都添加监听
for (URL url : urls) {
watch(url);
}
return urls;
}

总结

注册中心的设计比较简单,一个注册register和查找lookup就能简单满足要求。

为了提高性能和可用性,AbstractRegistry 还增加了本地缓存,其他实现继承 AbstractRegistry

最后我们讲了 ZkRegistry 的实现,主要就是注册查找监听

其他类型的注册中心按照这个模板,实现起来就会非常简单啦,如果有童鞋想实现其他的注册中心,欢迎给 ccx-rpc 提 PR。

ccx-rpc 代码已经开源

Github:https://github.com/chenchuxin/ccx-rpc

Gitee:https://gitee.com/imccx/ccx-rpc

从零开始实现简单 RPC 框架 4:注册中心的更多相关文章

  1. 从零开始实现简单 RPC 框架 2:扩展利器 SPI

    RPC 框架有很多可扩展的地方,如:序列化类型.压缩类型.负载均衡类型.注册中心类型等等. 假设框架提供的注册中心只有zookeeper,但是使用者想用Eureka,修改框架以支持使用者的需求显然不是 ...

  2. 从零开始实现简单 RPC 框架 6:网络通信之 Netty

    网络通信的开发,就涉及到一些开发框架:Java NIO.Netty.Mina 等等. 理论上来说,类似于序列化器,可以为其定义一套统一的接口,让不同类型的框架实现,事实上,Dubbo 就是这么干的. ...

  3. 从零开始实现简单 RPC 框架 3:配置总线 URL

    URL 的定义 URL 对于大部分程序猿来说都是很熟悉的,其全称是 Uniform Resource Locator (统一资源定位器).它是互联网的统一资源定位标志,也就是指网络地址. 一个标准的 ...

  4. 从零开始实现简单 RPC 框架 5:网络通信之序列化

    我们在接下来会开始讲网络通信相关的内容了.既然是网络通信,那必然会涉及到序列化的相关技术. 下面是 ccx-rpc 序列化器的接口定义. /** * 序列化器 */ public interface ...

  5. 从零开始实现简单 RPC 框架 7:网络通信之自定义协议(粘包拆包、编解码)

    当 RPC 框架使用 Netty 通信时,实际上是将数据转化成 ByteBuf 的方式进行传输. 那如何转化呢?可不可以把 请求参数 或者 响应结果 直接无脑序列化成 byte 数组发出去? 答:直接 ...

  6. 从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制

    一.心跳 什么是心跳 在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性.如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多 ...

  7. 从零开始实现简单 RPC 框架 8:网络通信之 Request-Response 模型

    Netty 在服务端与客户端的网络通信中,使用的是异步双向通信(双工)的方式,即客户端和服务端可以相互主动发请求给对方,发消息后不会同步等响应.这样就会有一下问题: 如何识别消息是请求还是响应? 请求 ...

  8. Java实现简单RPC框架(转)

    一.RPC简介 RPC,全称Remote Procedure Call, 即远程过程调用,它是一个计算机通信协议.它允许像本地服务一样调用远程服务.它可以有不同的实现方式.如RMI(远程方法调用).H ...

  9. RPC笔记之初探RPC:DIY简单RPC框架

    一.什么是RPC RPC(Remote Procedure Call)即远程过程调用,简单的说就是在A机器上去调用B机器上的某个方法,在分布式系统中极其常用. rpc原理其实很简单,比较容易理解,在r ...

随机推荐

  1. ctf实验吧Once More

    题目链接:http://ctf5.shiyanbar.com/web/more.php 思路分析:显然是后台逻辑代码. 1.ereg函数有漏洞,可以使用%00截断,这个就做笔记了好吧.这个函数大致意思 ...

  2. ctf实验吧逻辑问题

    ctf5.shiyanbar.com/web/5/index.php 绕开. php题,习惯先看源码,F12,结果发现了 url输入了一看 告诉了我们后台逻辑.分析一下,发现只要使得$row[&quo ...

  3. java面试一日一题:字节java后端工程师面试题

    今天来分享下字节一面面试题,各位小伙伴看看都能答上来吗,弄懂下面的问题你离字节又近了一步哦,加油吧 1.自我介绍: 2.问到项目中为什么选择hbase,如果有多个查询条件如何设置数据存储方案: 3.t ...

  4. ADC电阻分压采集相关知识

    1.电池串联可以增加电压,电池并联可以增加电流,都是为了增加电功率.但是电池不宜并联,即使是相同规格的电池,如果消耗不同,电势就会不同,会造成电池之间的放电现象. 2.电阻的作用: 限流:为使通过用电 ...

  5. Django基础07篇 ORM操作

    1.新增(类似数据库操作的insert) # 新增 #方式一: models.Category.objects.create(name='MySQL') #方式二: c = models.Catego ...

  6. ES6新增语法(三)——面向对象

    ES6中json的2个变化 简写:名字和值相同时,json可以可以简写 let a=12,b=5; let json = { a, b } console.log(json) // { a:12 , ...

  7. [004] - JavaSE面试题(四):JavaSE语法(2)

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [004] - JavaSE面试题(四):JavaSE语法(2) 第1问:重载(overload) ...

  8. 10分钟系列:NetCore3.1+EFCore三步快速完成数据库交互

    前言 做程序开发,不管是什么语言什么数据库,其中的ORM(对象关系映射)是必不可少的,但是不管选择哪一种ORM,都需要了解其中的运行机制,配置帮助类等等. 所以很多ORM都开始进行升级封装,我们只需要 ...

  9. springboot-3-web开发

    一.视图层技术thymeleaf 我们一般都是基于3.x版本 1.流程: 导入依赖 <!--整合thymeleaf技术--> <dependency> <groupId& ...

  10. 【洛谷P2800又上锁妖塔】动态规划

    分析 考虑上一层还是上两层还是爬上去 AC代码 #include <bits/stdc++.h> using namespace std; int f[1000005],a[1000005 ...