深度解析dubbo注册中心zookeeper实现

本文基于dubbo 2.6.x


一、ZookeeperRegistryFactory

ZookeeperRegistryFactory是注册中心zookeeper工厂的实现,代码比较简单,我们直接来看下。
在这里插入图片描述
可以看到ZookeeperRegistryFactory继承AbstractRegistryFactory抽象类,需要实现createRegistry方法,在createRegistry方法中,创建了一个zookeeper注册中心对象,参数分别是注册中心url与zookeeperTransporter (这个是dubbo spi 自动注入的,zookeeperTransporter能够获取zk客户端对象,这个文章后面会有说到)

二、ZookeeperRegistry

ZookeeperRegistry注册中心zk的实现,我们来看看它的源码实现

2.1 class与成员变量

public class ZookeeperRegistry extends FailbackRegistry {...}

可以看出ZookeeperRegistry继承FailbackRegistry失败重试抽象类,ZookeeperRegistry就需要实现doRegister,doUnregister,doSubscribe,doUnsubscribe 这几个方法(这个我们后面会一一解析)
接下来我们来看成员变量
在这里插入图片描述

2.2 构造

接下来看下构造方法:
在这里插入图片描述
在构造方法中,先是调用父类的构造,接着判断注册中心url的host是不是0.0.0.0(也就是任意host),下面就是获取group参数值,缺省是dubbo,如果不是"/“开头的就在开头拼接个”/",将group 赋值给root属性(其实这里很重要的,这个root就是在zk里面的根路径,如果你配置了组信息,将以你那个组信息值开头),接着就是使用zookeeperTransporter获取zk客户端对象,给zk客户端添加一个重连监听器,如果状态是重连状态,就调用recover方法来进行恢复,这里使用zookeeperTransporter获取zk客户端对象需要说下,zk的java客户端有两种,一个是curator,一个是zkclient,同时dubbo将这两种客户端一系列操作抽象了一个接口ZookeeperClient,curator与zkclient分别实现ZookeeperClient,这样做的好处就是统一了操作,方便开发,同时也增加了灵活性扩展性,使用zookeeperTransporter就是为了获取用户配置的ZookeeperClient实现,这里先看下ZookeeperClient实现关系
在这里插入图片描述
再来看下ZookeeperClient 抽象了哪些操作:

public interface ZookeeperClient {
    // 创建节点
    void create(String path, boolean ephemeral);
    // 删除节点
    void delete(String path);
    // 获取某个节点的子节点
    List<String> getChildren(String path);
    //为某个节点添加子节点监听器
    List<String> addChildListener(String path, ChildListener listener);
    // 移除某个节点的某个子节点监听器
    void removeChildListener(String path, ChildListener listener);
    // 添加 连接状态监听器
    void addStateListener(StateListener listener);
    // 移除 连接状态监听器
    void removeStateListener(StateListener listener);
    // 是否连接
    boolean isConnected();
    // 关闭
    void close();
    // 获取url
    URL getUrl();

}

zk客户端具体实现我们会后面说下。

2.3 doRegister

接着我们看下ZookeeperRegistry的doRegister进行注册方法实现:
在这里插入图片描述
我们可以看到使用zk客户端创建一个节点,这个节点的路径是toUrlPath(url) , 然后持久还是动态是由url中配置的dynamic参数值决定的,缺省是使用动态(所谓的动态就是会话级别的,断开连接,这个路径就没了)
这里我们更关心toUrlPath(url) 这个方法的实现:

// 将url转成 urlPath
private String toUrlPath(URL url) {
    //  "/dubbo/xxx.xxx.xxx.Xxxx/gategory(providers)/urlfullString"
    return toCategoryPath(url) + Constants.PATH_SEPARATOR + URL.encode(url.toFullString());
}

可以看到是toCategoryPath生成的一个path 拼接上 “/” ,再拼接上url的fullstring信息。接着在看下toCategoryPath方法

private String toCategoryPath(URL url) {
        // "/dubbo/xxx.xxx.xxx.Xxxx/gategory(providers)"
        return toServicePath(url) + Constants.PATH_SEPARATOR + url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
}

toCategoryPath方法是 toServicePath(url) 获取的path路径拼接上"/" ,在拼接上url的category分类信息,默认是providers

private String toServicePath(URL url) {
     String name = url.getServiceInterface();// 接口全类名
     if (Constants.ANY_VALUE.equals(name)) {
         return toRootPath();
     }
     //   "/dubbo/xxx.xxx.xxx.Xxxx"
     return toRootDir() + URL.encode(name);
}

toServicePath 中,先是获取接口全类名,如果接口全类名是*, 直接返回 root的值,也就是/dubbo 或者/你设置的group, 如果不是 ,就返回/dubbo/接口全类名
这个toUrlPath 方法全部拼接出来就是:

/dubbo(或者你设置的group属性值)/接口全类名(如果你接口全类名是`*` ,就没有这部分了)/ providers(也可以是consumers,routers,configurators 这个是根据你的category 属性值来的)/url的fullstring(这里其实就是url的 protocol, host,port,service,parameter等等)

这个就是你某个serivce在zk中的存在结构,我这里举个例子

/dubbo
/com.xuzhaocai.dubbo.provider.IHelloProviderService
/providers
/dubbo%3A%2F%2F192.162.0.174%3A18109%2Fcom.xuzhaocai.dubbo.provider.IHelloProviderService%3Faccesslog%3Dtrue%26anyhost%3Dtrue%26application%3Ddubbo-consumer%26bean.name%3DServiceBean%3Acom.xuzhaocai.dubbo.provider.IHelloProviderService%26dubbo%3D2.0.2%26generic%3Dfalse%26interface%3Dcom.xuzhaocai.dubbo.provider.IHelloProviderService%26methods%3DgetName%26pid%3D39753%26side%3Dprovider%26timestamp%3D1598241007805

在zk中大概就是下图这样一个结构(树结构):
在这里插入图片描述

2.4 doUnregister

接着我们再来看下doUnregister 下线方法实现:
在这里插入图片描述
这里就是调用zk客户端的delete方法删除url对应path的节点。

2.5 doSubscribe

接下来再来看下doSubscribe 进行订阅的方法(由于方法比较长,分为2部分):
第一部分是订阅所有
在这里插入图片描述
当url的service interface 是*的时候,表示要订阅所有,先是获取root,订阅所有,其实就是订阅根节点下面的所有子节点,接着从缓存中获取url对应的listener map ,这里为啥是map呢,其实一个url可以多个订阅者(也就可以有多个监听器),每个监听器又对应一个具体的子节点监听器。如果缓存中没有存在,就创建。如果没有获取到子节点监听器,也创建ChildListener对象,我们可以看看它里面的实现:

 @Override
public void childChanged(String parentPath, List<String> currentChilds) {
    for (String child : currentChilds) {// 遍历child
        child = URL.decode(child);
        if (!anyServices.contains(child)) {
            anyServices.add(child);
            // 订阅
            subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
                    Constants.CHECK_KEY, String.valueOf(false)), listener);
        }
    }
}

当订阅节点下面的子节点发生变化的时候,就会调用childChanged 方法,parentPath 表示父节点,也就是该节点下面的子节点发生了变化,currentChilds表示当前parentPath 节点下面所有的子节点集合。在childChanged方法中,对子节点集合进行遍历,然后如果这个子节点没有在anyServices 集合中,就添加进去(anyServices 集合 就是存的所有的接口全类名,也就是根节点下的所有子节点),接着就是订阅这个子节点。
doSubscribe 方法接着往下看,使用zk客户端创建root节点,对root节点添加监听器listener,获取到root节点(也就是根节点 比如/dubbo)子节点集合,遍历集合添加到anyServices 集合中,然后订阅这堆子节点。
接着再看看第二部分,订阅某个接口的时候。
在这里插入图片描述
该部分中,先是通过toCategoriesPath获取paths,这个paths的个数其实是由url的category属性决定的 这个path就是上面说的这个,需要注意的是,这里path是没有url.tofullstring 那部分的,因为我们这订阅的是它的父节点,当下面子节点发生变动的时候就会通知。

/dubbo(或者你设置的group属性值)/接口全类名(如果你接口全类名是`*` ,就没有这部分了)/ providers(也可以是consumers,routers,configurators 这个是根据你的category 属性值来的)

如果你的category属性值是*它就创建4个,分别是providers,routers,consumers,configurators,不是*就去你设置的那个,缺省是providers。接着就是根据url从zkListeners缓存中获取,如果没有的话就创建,跟上面一样,但是它这里创建的子节点监视器ChildListener 对象与上面那部分不一样(这里很重要

@Override
public void childChanged(String parentPath, List<String> currentChilds) {
    ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}

当订阅的节点子类发生变化的时候,就会调用监听器的childChanged 方法,在childChanged 方法 中调用了notify方法进行通知。
再往下看,就是创建这个path,在path上面添加监听器,能够获取现在path下面的子节点,然后把这些子节点经过过滤后添加到list中。
最后遍历完成后,调用notify通知(这里这样的做的原因是第一次订阅的时候,如果不通知,需要子节点变动的时候才能通知,才能获取到服务提供者列表),这里面有个toUrlsWithEmpty(url, path, children) 方法,该方法将 字符串子节点转成url集合,其实这个方法中有个比较重要的点就是过滤,需要与consumerUrl的某些条件匹配上。我们来看下:
在这里插入图片描述
开始调用toUrlsWithoutEmpty(consumer, providers); 方法将字符串子节点转成url集合,我们来看下这个方法
在这里插入图片描述
这里就是遍历providers,然后需要两个条件才能添加到结果集合中,一个是判断 里面有没有:// 这个东西就是放在协议protocol后面的,这个有之后 ,将字符串就转成了url,然后调用UrlUtils.isMatch(consumer, url)方法进行匹配,匹配的上才能添加到结果集合中,这个isMatch 方法主要比对双方的 接口,category,enable,group,version,classifier 这些信息:
在这里插入图片描述
再回到toUrlsWithEmpty 方法中,转换成urls之后,如果是空的,就生成一个protocol 是empty的url塞进去返回,为什么要生成一个empty的url(这玩意就是代表着注册中心没有该consumer的实现了,本地缓存也就需要清空),原因是listener实现类notify方法中,看到就这一条empty url的时候就会清空对应的category下的url缓存。
到这我们的执行订阅doSubscribe 方法就讲解完成了

2.6 doUnsubscribe

接下来我们看下doUnsubscribe 执行取消订阅的方法的实现:
在这里插入图片描述
这个比较简单,就是从缓存zkListeners 中获取对应的listener map集合,然后再获取listener对应的ChildListener监听器,如果不是空的话,判断接口是不是*,如果是,就调用zk客户端的removeChildListener方法移除根节点下的对应listener。
如果不是,就循环移除分组path的对应listener。

三、总结

本文主要讲解了 注册中心zk工厂的实现 ZookeeperRegistryFactory 与 注册中心zk 的实现ZookeeperRegistry ,其中注册中心zk 实现ZookeeperRegistry 是非常重要的,在这个类中并不是直接操作的zk,而是dubbo抽象了zk客户端 curator与 zkClient两种实现的一些操作,通过dubbo spi 来根据配置获得具体的实现(我们会单独解析zk客户端实现的),ZookeeperRegistry 中主要是注册到zk里面的树结构,订阅方法doSubscribe是很重要的,尤其是一些细节,比如说创建的节点监听 ,当子节点变动的时候,就会调用notify方法进行通知。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页