热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

dubbo+zookeeper示例代码_不知道如何实现服务的动态发现?快来看看Dubbo是如何做到的

上篇文章如果有人问你Dubbo中注册中心工作原理,就把这篇文章给他大致了解了注册中心作用以及DubboRegistry模块源码,这篇文章将深入Dubbo
e3bd684e1d450b5ed83cbcf835762845.png

上篇文章如果有人问你 Dubbo 中注册中心工作原理,就把这篇文章给他大致了解了注册中心作用以及 Dubbo Registry 模块源码,这篇文章将深入 Dubbo ZooKeeper 模块,去了解如何实现服务动态的发现。

ps: 以下将 ZooKeeper 缩写为 zk。

一、dubbo zk 数据结构

在 ZooKeeper 基本概念分享一文讲道,ZK 内部是一种树形层次结构,节点存在多种类型。而 Dubbo 只会创建持久节点和临时节点。

若服务提供者服务接口为 com.service.FooService,将会在 ZK 中创建创建如下路径 /dubbo/com.service.FooService/providers/providerURL

服务路径分为四层,根节点默认为 dubbo,可以在 dubbo-registry 设置 group 属性改变该值。

ps: 若无注册中心隔离需求,不要随便修改。

第二层节点为服务节点全名称,如 com.service.FooService

第三层节点为服务目录,如 providers。另外还存在其他目录节点,分别为 consumers(消费者目录),configurators(配置目录),routers(路由目录)。下面服务订阅主要针对这一层节点。

第四个节点为具体服务节点,节点名为具体的 URL 字符串,如 dubbo://2.0.1.13:12345/com.dubbo.example.DemoService?xx=xx ,该节点默认为临时节点。 dubbo ZK 树形内部结构示例为:

36c5b29bfe2ecf765d89ccbfa5b911e7.png

ZK 内部服务具体示例如下:

190fbcd654854510a49dee40c52416b1.png

二、RegistryFactory 实现

Dubbo 可以在配置文件中指定使用注册中心,可以使用 dubbo.registry.protocol 指定具体注册中心类型,也可以设置 dubbo.registry.address 指定。注册中心相关实现将会使用 RegistryFactory 工厂类创建。

RegistryFactory 接口源码如下:

@SPI("dubbo")
public interface RegistryFactory {@Adaptive({"protocol"})Registry getRegistry(URL url);
}

RegistryFactory 接口方法使用 @Adaptive 注解,这里将会使用 Dubbo SPI 机制,自动生成代码的一些实现逻辑。这里将会根据 URL 中 protocol 属性,去调用最终实现子类。

RegistryFactory 实现子类如图所示:

8bf8791f6b096b809206d83102aa6f54.png

AbstractRegistryFactory 将会实现接口的 getRegistry 方法,主要完成加锁,并调用抽象模板方法 createRegistry 创建具体注册中心实现类,并将其缓存在内存中。

AbstractRegistryFactory#getRegistry 源码如下所示:

public Registry getRegistry(URL url) {url = URLBuilder.from(url).setPath(RegistryService.class.getName()).addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName()).removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY).build();String key = url.toServiceStringWithoutResolving();// 加锁,防止并发LOCK.lock();try {// 先从缓存中取Registry registry = REGISTRIES.get(key);if (registry != null) {return registry;}//使用 Dubbo SPI 进制创建registry = createRegistry(url);if (registry == null) {throw new IllegalStateException("Can not create registry " + url);}// 放入缓存REGISTRIES.put(key, registry);return registry;} finally {// Release the lockLOCK.unlock();}}

注册中心实例将会通过具体工厂类创建,这里我们看下 ZookeeperRegistryFactory 源码:

public class ZookeeperRegistryFactory extends AbstractRegistryFactory {private ZookeeperTransporter zookeeperTransporter;/*** 通过 Dubbo SPI 进制注入* @param zookeeperTransporter*/public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {this.zookeeperTransporter = zookeeperTransporter;}@Overridepublic Registry createRegistry(URL url) {return new ZookeeperRegistry(url, zookeeperTransporter);}}

ps:Dubbo SPI 机制还具有 IOC 特性,这里的ZookeeperTransporter 注入可以参考:Dubbo 扩展点加载

三、zk 模块源码解析

讲完注册中心实例创建过程,下面深入 ZookeeperRegistry 实现源码。

ZookeeperRegistry 继承 FailbackRegistry抽象类,所以其需要实现其父类抽象模板方法,下面主要了解 doRegisterdoSubscribe源码 。

3.1 doRegister

服务提供者需要将服务注册到注册中心,注册的目的是为了让消费者感知到服务的存在,从而发起远程调用,另一方面也让服务治理中心感知新的服务提供者上线。zk 模块服务注册代码比较简单,直接使用 zk 客户端在注册中心创建节点。

ZookeeperRegistry#doRegister 实现源码如下:

public void doRegister(URL url) {try {zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));} catch (Throwable e) {throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);}}

zkClient.create 方法需要传入两个参数。

void create(String path, boolean ephemeral);

第一个参数为节点路径,将会通过 toUrlPath 将 URL 实例转化成 ZK 中路径格式,转化结果如下:

## 转化前 URL 如下:dubbo://10.20.82.31:12345/com.dubbo.example.DemoService## 调用 `toUrlPath` 转换之后
/dubbo/com.dubbo.example.DemoService/providers/dubbo%3A%2F%2F10.20.82.31%3A12345%2Fcom.dubbo.example.DemoService

第二个参数主要决定 ZK 节点类型主要取自 URL 实例对象中 dynamic 参数值,若不存在,默认为 true,也就是默认将会创建临时节点。

zkClient.create 方法里将会递归调用,首先父节点是否存在,不存在就会创建,直到最后一个节点跳出递归方法。

public void create(String path, boolean ephemeral) {// 创建永久节点之前需要判断是否已存在if (!ephemeral) {if (checkExists(path)) {return;}}// 判断是否存在父节点int i = path.lastIndexOf('/');if (i > 0) {// 递归创建父节点create(path.substring(0, i), false);}if (ephemeral) {// 创建临时节点createEphemeral(path);} else {// 创建永久节点createPersistent(path);}}

最后 createEphemeralcreatePersistent 实际创建节点操作将会交给 ZK 客户端类,这里实现比较简单,可以自行参考源码。

ps: dubbo 在 2.6.1 起将 zk 客户端默认使用 Curator,之前版本使用 zkclient。dubbo 2.7.1 开始去除 zkclient 实现,也就是说只能使用 Curator 。

3.2 为何 dubbo 服务提供者节点使用 zk 临时节点

zk 临时节点将会在 zk 客户端断开后,自动删除。dubbo 服务提供者正常下线,其会主动删除 zk 服务节点。

如果服务异常宕机,zk 服务节点就不能正常删除,这就导致失效的服务一直存在 ZK 上,消费者还会调用该失效节点,导致消费者报错。通过 zk 临时节点特性,让 zk 服务端主动删除失效节点,从而下线失效服务。

四、doSubscribe: 服务动态发现的原理

4.1 订阅基本原理

服务订阅通常有 pull 和 push 两种方式。pull 模式需要客户端定时向注册中心拉取配置,而 push 模式采用注册中心主动推送数据给客户端。

dubbo zk 注册中心采用是事件通知与客户端拉取方式。服务第一次订阅的时候将会拉取对应目录下全量数据,然后在订阅的节点注册一个 watcher。一旦目录节点下发生任何数据变化,zk 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据,并重新注册 watcher。利用这个模式,dubbo 服务就可以就做到服务的动态发现。

4.2 源码解析

讲完订阅的基本原理,接着深入源码。

doSubscribe 方法需要传入两个参数,一个为 URL 实例,另一个为 NotifyListener,变更事件的监听器。 方法内部会根据 URL 接口类型分成两部分逻辑,全量订阅服务与部分类别订阅服务。

doSubscribe 方法整体源码逻辑:

public void doSubscribe(final URL url, final NotifyListener listener) {if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {// 全量订阅逻辑} else {// 部分类别订阅逻辑}}

服务治理中心(dubbo-admin),需要订阅 service 全量接口,用以感知每个服务的状态,所以订阅之前将会把 service 设置成 *,处理所有service。

服务消费者或服务提供者将会走部分类别订阅服务,下面我们以消费者视角,深入后续源码。

文章刚开头讲道了 zk 目录节点存在四种类型,这里将会根据 根据 URL 中 category值,决定订阅节点路径。

服务提供者 URL 中 category值默认为 configurators,而消费者 URL 中category值默认为 providers,configurators,routers。如果 category类别值为 *,将会订阅四种类别路径,否则将会只订阅 providers类型的路径。

toCategoriesPath 源码如下:

private String[] toCategoriesPath(URL url) {String[] categories;// 如果类别为 *,订阅四种类型的全量数据if (Constants.ANY_VALUE.equals(url.getParameter(Constants.CATEGORY_KEY))) {categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};} else {categories = url.getParameter(Constants.CATEGORY_KEY, new String[]{Constants.DEFAULT_CATEGORY});}// 返回路径数组String[] paths = new String[categories.length];for (int i = 0; i

接着循环路径数组,循环内将会缓存节点监听器,用以提高性能。

// 循环路径数组for (String path : toCategoriesPath(url)) {ConcurrentMap listeners &#61; zkListeners.get(url);// listeners 缓存为空&#xff0c;创建缓存if (listeners &#61;&#61; null) {zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());listeners &#61; zkListeners.get(url);}ChildListener zkListener &#61; listeners.get(listener);// zkListener 缓存为空则创建缓存if (zkListener &#61;&#61; null) {listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));zkListener &#61; listeners.get(listener);}// 创建订阅节点zkClient.create(path, false);// 使用 ZK 客户端订阅节点List children &#61; zkClient.addChildListener(path, zkListener);if (children !&#61; null) {// 存储全量需要通知的 URLurls.addAll(toUrlsWithEmpty(url, path, children));}}// 回调 NotifyListenernotify(url, listener, urls);

最终将会使用 CuratorClient.getChildren().usingWatcher(listener).forPath(path) 在 ZK 节点注册 watcher&#xff0c;并获取目录节点下所有子节点数据。

这里 watcher 使用 Curator 接口 CuratorWatcher&#xff0c;一旦 ZK 节点发生会变化&#xff0c;将会回调 CuratorWatcher#process 方法。

CuratorWatcher#process 方法源码如下&#xff1a;

public void process(WatchedEvent event) throws Exception {if (childListener !&#61; null) {String path &#61; event.getPath() &#61;&#61; null ? "" : event.getPath();childListener.childChanged(path,// 重新设置 watcher&#xff0c;并获取节点下所有子节点StringUtils.isNotEmpty(path)? client.getChildren().usingWatcher(this).forPath(path): Collections.emptyList());}}

消费者订阅时序图如下&#xff1a;

4.3 listener 关系图

订阅方法中我们碰到了多个 listener类&#xff0c;刚开始理解时候可能有点乱。可以参考下面关系图理清楚这其中的关系。

listener 关系图如下&#xff1a;

dac0fbe59b1c64147f51006aa21cf18a.png

回调关系如图所示&#xff1a;

4.4 ZK 模块订阅存在问题

ZK 第一次订阅将会获得目录节点下所有子节点&#xff0c;后续任意子节点变更&#xff0c;将会通过 watcher 进制回调通知。回调通知将会再次全量拉取节点目录下所有子节点。这样全量拉取将会有个局限&#xff0c;当服务节点较多时将会对网络造成很大的压力。

Dubbo 2.7 之后版本引入元数据中心解决该问题&#xff0c;详情可参考&#xff0c;阿里技术专家详解 Dubbo 实践&#xff0c;演进及未来规划。

引用文中一种解决方案如下图:

eb5e0492f051085822e18b95b01cc40c.png
cc4e8ca2287220bf1a6ee29298c39c0f.png

总结

本文主要介绍了 dubbo zk 的数据结构&#xff0c;其次深入研究 ZookeeperRegistry 相关实现源码。通过了解服务注册以及订阅原理&#xff0c;了解 Dubbo 服务动态发现实现方式。



推荐阅读
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
author-avatar
流域
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有