从零开始实现简单 RPC 框架 4:注册中心
RPC 中服务消费端(Consumer
) 需要请求服务提供方(Provider
)的接口,必须要知道 Provider
的地址才能请求到。
那么,Consumer
要从哪里获取 Provider
的地址呢?
能不能 Consumer
自己配置 Provider
的地址?
这种方式理论上是可行的,不过事实上没人这么做。这种方式有以下缺点:
Consumer
每引用一个接口,需要配置一次Provider
的服务地址,配置繁琐易错。Consumer
引用其他业务组的服务,需要跨团队沟通,沟通成本高。Provider
如果换服务器、挂掉、新增,都需要通知到Consumer
去修改服务地址,配置修改可能不及时造成服务异常。Consumer
如果引用很多服务,那么配置会非常杂乱,管理起来非常麻烦。
从上面的缺点来看,最好的方式是找个地方把配置管理起来。
例如,把配置放到统一的数据库中,Provider
启动的时候,把自己的地址和接口写到表中; Consumer
在请求接口之前,就可以从表里获取该接口对应的Provider
地址。
其实,这种把配置统一管理的地方,就叫 注册中心
注册中心就像中间桥梁,连接Provider
和Consumer
。三方关系示意图如下:
注册中心 只是 Provider
感知 Consumer
的一种方式而已,最终 Provider
调用 Consumer
接口还是以直连的方式进行。
Provider
注册或者取消注册,注册中心会通知 Consumer
,保证 Consumer
感知服务状态的及时性。
注册中心的特性#
一个合格的注册中心,需要有以下的特性:
1. 存储#
可以简单地将注册中心理解为一个存储系统,存储着服务与服务提供方的映射表。一般注册中心对存储没有太多特别的要求,甚至夸张一点,你可以基于数据库来实现一个注册中心。
2. 高可用#
注册中心一旦挂掉,Consumer
将无法获取 Provider
的地址,整个微服务将无法运转。
当然 Consumer
可以添加本地缓存,从某种角度上看,是允许注册中心短暂挂掉的。
3. 健康检查#
Provider
向注册中心注册服务之后,注册中心需要定时向 Provider
发起健康检查,当 Provider
宕机的时候,注册中心能更快发现 ,从而将宕机的 Provider
从注册表中移除。
这特性数据库、Redis 都不具有,因此他们不适合做注册中心。
4. 监听状态#
当服务增加、减少 Provider
的时候,注册中心除了能及时更新,还要能主动通知 Consumer
,以便 Consumer
能快速更新本地缓存,减少错误请求的次数。
这一特性同样数据库、Redis都不具有。
目前主流的注册中心有:Zookeeper
、Eureka
、Nacos
、Consul
等。
由于本文主要是讲注册中心的实现,就不详细讲各种注册中心的差异、优缺点了,有兴趣的同学可以看这里
下面我们来讲 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
本地缓存不存在的时候,会请求到 ZkRegistry
的 doLookup
,请求出来的 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
作者:小新是也
出处:https://www.cnblogs.com/chenchuxin/p/15178298.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略