Dubbo注冊(cè)中心

Dubbo注冊(cè)中心

前言

本篇主要介紹一下Dubbo的注冊(cè)中心的總體工作流程愈涩,以及不同類型注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)原理,同時(shí)還會(huì)介紹一下注冊(cè)中心支持的通用特性慢显,如緩存機(jī)制持际、重試機(jī)制,最會(huì)會(huì)對(duì)整個(gè)注冊(cè)中心中用到的設(shè)計(jì)模式做深度的解析殴蹄,主要目的是為了深入理解Dubbo各種注冊(cè)中心的實(shí)現(xiàn)原理究抓,方便后續(xù)快速理解并拓展。

概述

注冊(cè)中心是Dubbo體系中核心組件之一袭灯,通過(guò)注冊(cè)中心實(shí)現(xiàn)了再分布式環(huán)境中各服務(wù)之間的注冊(cè)發(fā)現(xiàn)刺下,其主要作用如下:

  • 動(dòng)態(tài)加入:一個(gè)服務(wù)提供者通過(guò)注冊(cè)中心動(dòng)態(tài)地把自己暴露給其他消費(fèi)者,無(wú)需消費(fèi)者挨個(gè)去更新配置文件稽荧。
  • 動(dòng)態(tài)發(fā)現(xiàn):一個(gè)消費(fèi)者可以動(dòng)態(tài)的感知新的配置橘茉,路由規(guī)則和新的服務(wù)提供者,無(wú)需重啟服務(wù)就可以生效。
  • 動(dòng)態(tài)調(diào)整:注冊(cè)中心支持參數(shù)的動(dòng)態(tài)調(diào)整畅卓,新參數(shù)自動(dòng)更新到所有相關(guān)服務(wù)節(jié)點(diǎn)擅腰。
  • 統(tǒng)一配置:避免了本地配置導(dǎo)致各個(gè)服務(wù)的配置不一致的問(wèn)題。

模塊介紹

當(dāng)前dubbo版本: 3.0.4

模塊名稱 模塊介紹
dubbo-registry-api 包含注冊(cè)中心所有API和抽象實(shí)現(xiàn)類
dubbo-registry-kubernetes 在kubernetes中注冊(cè)中心的實(shí)現(xiàn)
dubbo-registry-dns 使用dns作注冊(cè)中心的實(shí)現(xiàn)
dubbo-registry-multicast multicast模式的服務(wù)的注冊(cè)和發(fā)現(xiàn)
dubbo-registry-multiple 多注冊(cè)中心模式下服務(wù)的注冊(cè)和發(fā)現(xiàn)
dubbo-registry-nacos 使用Nacos作為注冊(cè)中心的實(shí)現(xiàn)
dubbo-registry-xds Service-mesh模式下服務(wù)的注冊(cè)和發(fā)現(xiàn)(Service mesh)
dubbo-registry-zookeeper 使用ZooKeeper作為注冊(cè)中心的實(shí)現(xiàn)

從dubbo-registry的模塊中我們可以看到髓介,Dubbo主要包含6種注冊(cè)中心的實(shí)現(xiàn)惕鼓,分別是:kubernetes、dns唐础、multicast箱歧、nacos、xds一膨、zookeeper呀邢。

下面我們來(lái)看下各種維度的對(duì)比

工作流程

注冊(cè)紅心的總體工作流程比較簡(jiǎn)單,總體流程如下圖所示:

整體流程.drawio.png
  • 服務(wù)提供者服務(wù)啟動(dòng)時(shí)豹绪,會(huì)向注冊(cè)中心寫入自己的元數(shù)據(jù)信息价淌,同時(shí)會(huì)訂閱配置元數(shù)據(jù)信息。
  • 消費(fèi)者啟動(dòng)時(shí)瞒津,也會(huì)想注冊(cè)中心寫入自己的元數(shù)據(jù)信息蝉衣,并訂閱服務(wù)提供者、路由和配置元數(shù)據(jù)信息巷蚪。
  • 服務(wù)治理中心(dubbo-admin)啟動(dòng)時(shí)病毡,會(huì)同時(shí)訂閱所有的消費(fèi)者、服務(wù)提供者屁柏、路由和配置元數(shù)據(jù)信息啦膜。
  • 當(dāng)有服務(wù)提供者下線或者有新的服務(wù)提供者加入時(shí),注冊(cè)中心服務(wù)提供者目錄會(huì)發(fā)生變化淌喻,變化信息會(huì)動(dòng)態(tài)給消費(fèi)者僧家、服務(wù)治理中心。
  • 當(dāng)消費(fèi)者發(fā)起服務(wù)調(diào)用時(shí)裸删,會(huì)異步將調(diào)用八拱、統(tǒng)計(jì)信息等上報(bào)給監(jiān)控中心(dubbo-monitor-simple)。

數(shù)據(jù)結(jié)構(gòu)

注冊(cè)中心的總體流程大致類似涯塔,但是不同注冊(cè)中心有不同的實(shí)現(xiàn)方式肌稻,其數(shù)據(jù)結(jié)構(gòu)也不相同。ZooKeeper伤塌、Nacos等注冊(cè)中心都實(shí)現(xiàn)了這個(gè)流程,又要有些注冊(cè)中心并不常用轧铁,因此本篇只分析ZooKeeper每聪、Nacos兩種實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)。

ZooKeeper原理概述

ZooKeeper是樹形結(jié)構(gòu)的注冊(cè)中心,每個(gè)節(jié)點(diǎn)的類型分為持久化節(jié)點(diǎn)药薯、持久化順序節(jié)點(diǎn)绑洛、臨時(shí)節(jié)點(diǎn)、臨時(shí)順序節(jié)點(diǎn)童本。

  • 持久化節(jié)點(diǎn):服務(wù)注冊(cè)后保證節(jié)點(diǎn)不會(huì)丟失真屯,注冊(cè)中心重啟也會(huì)存在。
  • 持久化順序節(jié)點(diǎn):在持久節(jié)點(diǎn)特性的基礎(chǔ)上增加了節(jié)點(diǎn)先后順序的能力穷娱。
  • 臨時(shí)節(jié)點(diǎn):服務(wù)注冊(cè)后連接丟失或session超時(shí)绑蔫,注冊(cè)的節(jié)點(diǎn)會(huì)自動(dòng)被移除。
  • 臨時(shí)順序節(jié)點(diǎn):在臨時(shí)節(jié)點(diǎn)的特性上增加了節(jié)點(diǎn)先后順序的能力泵额。

Dubbo使用zk作為注冊(cè)中心時(shí)配深,只會(huì)創(chuàng)建持久化節(jié)點(diǎn)和臨時(shí)節(jié)點(diǎn)兩種,對(duì)創(chuàng)建的順序并沒(méi)有要求嫁盲。

/dubbo/org.apache.dubbo.demo.DemoService/providers是服務(wù)提供者在ZooKeeper注冊(cè)中心的路徑示例篓叶,是一種樹形結(jié)構(gòu),該結(jié)構(gòu)分為四層:root(根節(jié)點(diǎn)羞秤,對(duì)應(yīng)示例中的dubbo)缸托、service(接口名稱,對(duì)應(yīng)示例中的org.apache.dubbo.demo.DemoService)瘾蛋、四種服務(wù)目錄(對(duì)應(yīng)示例中的providers俐镐,其他目錄為consumers、routers瘦黑、configurations)京革。樹形結(jié)構(gòu)如下:

++ /dubbo

++-- xxxservice

    +-- providers

    +-- consumers

    +-- routers

    +-- configurators

樹形結(jié)構(gòu)的關(guān)系:

  1. 樹的節(jié)點(diǎn)是注冊(cè)中心分組。下面有多個(gè)服務(wù)接口幸斥,分組值分別來(lái)自用戶配置<dubbo:registry>中的group屬性匹摇,默認(rèn)是/dubbo。
  2. 服務(wù)接口下包含四類子目錄甲葬,分別是providers廊勃,consumers,routers经窖,configurations這個(gè)路徑是持久化節(jié)點(diǎn)坡垫。
  3. 服務(wù)提供者目錄(/dubbo/service/providers)下面包含的接口有多個(gè)服務(wù)者URL元數(shù)據(jù)信息。
  4. 服務(wù)消費(fèi)者目錄(/dubbo/service/consumers)下面包含的接口有多個(gè)消費(fèi)者URL元數(shù)據(jù)信息画侣。
  5. 路由配置目錄(/dubbo/service/routers)下面包含多個(gè)用于消費(fèi)者路由策略URL元數(shù)據(jù)信息冰悠。
  6. 動(dòng)態(tài)配置目錄(/dubbo/service/configurators)下面包含多個(gè)用于服務(wù)者動(dòng)態(tài)配置URL元數(shù)據(jù)信息。

大致的存儲(chǔ)結(jié)構(gòu)為:

zookeeper服務(wù)信息結(jié)構(gòu).drawio.png
  1. 在Dubbo框架啟動(dòng)時(shí)配乱,會(huì)根據(jù)用戶配置的服務(wù)溉卓,在注冊(cè)中心創(chuàng)建四個(gè)目錄皮迟,在providers和consumers目錄中分別存儲(chǔ)服務(wù)提供方、消費(fèi)方元數(shù)據(jù)信息桑寨,主要包括IP伏尼、端口、權(quán)重和應(yīng)用名等數(shù)據(jù)尉尾。
  2. 在Dubbo框架進(jìn)行服務(wù)調(diào)用時(shí)爆阶,用戶可以通過(guò)服務(wù)治理平臺(tái)(dubbo-admin)下發(fā)路由配置。如果要運(yùn)行時(shí)改變服務(wù)參數(shù)沙咏,則用戶可以通過(guò)服務(wù)治理平臺(tái)(dubbo-admin)下發(fā)動(dòng)態(tài)配置辨图。服務(wù)端會(huì)通過(guò)訂閱機(jī)制收到屬性變更,并重新更新已經(jīng)暴露的服務(wù)芭碍。

Nacos原理概述

不同的是在Nacos 中徒役,服務(wù)注冊(cè)時(shí)在服務(wù)端本地會(huì)通過(guò)輪詢注冊(cè)中心集群節(jié)點(diǎn)地址進(jìn)行服務(wù)得注冊(cè),在注冊(cè)中心上窖壕,即Nacos Server上采用了Map保存實(shí)例信息忧勿,當(dāng)然配置了持久化的服務(wù)會(huì)被保存到數(shù)據(jù)庫(kù)中,在服務(wù)的調(diào)用方瞻讽,為了保證本地服務(wù)實(shí)例列表的動(dòng)態(tài)感知鸳吸,Nacos與其他注冊(cè)中心不同的是,采用了 Pull/Push同時(shí)運(yùn)作的方式速勇。通過(guò)這些我們對(duì)Nacos注冊(cè)中心的原理有了一定的了解晌砾。

訂閱與發(fā)布

訂閱/發(fā)布是整個(gè)注冊(cè)中心的核心功能之一。與傳統(tǒng)系統(tǒng)應(yīng)用的差別:配置變化時(shí)烦磁,無(wú)需手動(dòng)觸發(fā)配置重新加載养匈,自動(dòng)化運(yùn)維。

當(dāng)我們使用注冊(cè)中心都伪,一個(gè)服務(wù)節(jié)點(diǎn)下線或者新增一個(gè)服務(wù)提供者節(jié)點(diǎn)呕乎,訂閱對(duì)應(yīng)接口的消費(fèi)者和治理中心就能及時(shí)厚道注冊(cè)中心的通知,并更新本地配置信息陨晶。整個(gè)過(guò)程都是自動(dòng)完成猬仁,無(wú)需人工參與。

Dubbo在上層抽象類這樣一個(gè)共走流程先誉,但可以有不同實(shí)現(xiàn)湿刽。本篇主要講ZooKeeper和Nacos的實(shí)現(xiàn)方式。

ZooKeeper的實(shí)現(xiàn)

發(fā)布的實(shí)現(xiàn)

服務(wù)提供者和消費(fèi)者都需要把自己注冊(cè)到注冊(cè)中心褐耳,服務(wù)提供者的注冊(cè)是為了讓消費(fèi)者感知服務(wù)的存在诈闺,從而發(fā)起遠(yuǎn)程調(diào)用,也讓服務(wù)治理中心感知有新的服務(wù)提供者上線铃芦。消費(fèi)者的發(fā)布是為了讓服務(wù)治理中心也可以發(fā)現(xiàn)自己雅镊。ZooKeeper發(fā)布代碼非常簡(jiǎn)單把曼,只是調(diào)用ZooKeeper的客戶端再注冊(cè)中心創(chuàng)建了一個(gè)目錄,代碼如下:

創(chuàng)建目錄

zkClient.create(toUrlPath(url));
url.getParameter(DYNAMIC_KEY, true)

刪除路徑

zkClient.delete(toUrlPath(url));

訂閱的實(shí)現(xiàn)

訂閱通常有pull和push兩種方式漓穿,一種是客戶端定時(shí)輪詢注冊(cè)中心拉取配置,另一種是注冊(cè)中心主動(dòng)推送數(shù)據(jù)給客戶端注盈。這兩種方式各有利弊晃危,目前dubbo采用的是第一次啟動(dòng)拉取方式,后續(xù)接收時(shí)間重新拉取數(shù)據(jù)老客。

在服務(wù)暴露時(shí)僚饭,服務(wù)端會(huì)訂閱configurators用于監(jiān)聽(tīng)動(dòng)態(tài)配置,在消費(fèi)者啟動(dòng)時(shí)胧砰,消費(fèi)端會(huì)訂閱providers鳍鸵、routers和configurators這三個(gè)目錄,分別對(duì)一個(gè)服務(wù)提供者尉间、路由和動(dòng)態(tài)配置變化通知偿乖。

Dubbo中有哪些ZooKeeper客戶端實(shí)現(xiàn)?

  • Apache Curator
  • zkClient

ZooKeeper注冊(cè)中心采用的是 事件通知 + 客戶端拉取的方式哲嘲,客戶端在第一次連接上注冊(cè)中心時(shí)贪薪,會(huì)獲取對(duì)應(yīng)目錄下全量的數(shù)據(jù),并在訂閱的節(jié)點(diǎn)上注冊(cè)一個(gè)watcher眠副,客戶端與注冊(cè)中心之間保持TCP長(zhǎng)連接画切,后續(xù)每個(gè)節(jié)點(diǎn)有任何數(shù)據(jù)變化的時(shí)候,注冊(cè)中心會(huì)根據(jù)watcher的回調(diào)通知客戶端(事件通知)囱怕,客戶端接收到通知霍弹,會(huì)把對(duì)應(yīng)節(jié)點(diǎn)的全量數(shù)據(jù)都拉取過(guò)來(lái)(客戶端拉取)娃弓,代碼中再NotifyListener#notify(List<URL> urls)接口上就有約束的注釋說(shuō)明典格。

注意:全量拉取有局限性,當(dāng)微服務(wù)節(jié)點(diǎn)較多會(huì)對(duì)網(wǎng)絡(luò)造成很大壓力忘闻。

ZooKeeper每個(gè)節(jié)點(diǎn)都有一個(gè)版本號(hào)钝计,當(dāng)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變化(即事務(wù)操作)時(shí),該節(jié)點(diǎn)的版本號(hào)就會(huì)發(fā)生變化齐佳,并觸發(fā)watcher事件私恬,推送數(shù)據(jù)給訂閱方。版本號(hào)強(qiáng)調(diào)的是變更次數(shù)炼吴,即使該節(jié)點(diǎn)的值沒(méi)有變化本鸣,只要有更新操作,依然會(huì)使版本號(hào)變化硅蹦。

ZooKeeper全量訂閱服務(wù)代碼分析荣德,核心代碼來(lái)自ZookeeperRegistry#doSubscribe闷煤,代碼如下:

if (ANY_VALUE.equals(url.getServiceInterface())) {
                String root = toRootPath();
                boolean check = url.getParameter(CHECK_KEY, false);
  //listeners為空說(shuō)明緩存中沒(méi)有就初始化一個(gè)空map
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
  //這里把listeners放入緩存中
                ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> {
                  //內(nèi)部方法 不會(huì)立即執(zhí)行,只會(huì)在觸發(fā)通知時(shí)執(zhí)行
                    for (String child : currentChilds) {
                      //如果子節(jié)點(diǎn)有變化則會(huì)接到新通知涮瞻,遍歷所有的子節(jié)點(diǎn)
                        child = URL.decode(child);
                      //如果存在子節(jié)點(diǎn)還沒(méi)有被訂閱鲤拿,說(shuō)明是新節(jié)點(diǎn),則訂閱
                        if (!anyServices.contains(child)) {
                            anyServices.add(child);
                            subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
                                Constants.CHECK_KEY, String.valueOf(check)), k);
                        }
                    }
                });
                            //創(chuàng)建持久化節(jié)點(diǎn)署咽,接下來(lái)訂閱持久化節(jié)點(diǎn)的直接子節(jié)點(diǎn)
                zkClient.create(root, false);
                List<String> services = zkClient.addChildListener(root, zkListener);
                if (CollectionUtils.isNotEmpty(services)) {
                  //遍歷所有子節(jié)點(diǎn)進(jìn)行訂閱
                    for (String service : services) {
                        service = URL.decode(service);
                        anyServices.add(service);
                        //增加當(dāng)前節(jié)點(diǎn)的訂閱近顷,并且會(huì)返回該節(jié)點(diǎn)下所有子節(jié)點(diǎn)列表
                        subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
                            Constants.CHECK_KEY, String.valueOf(check)), listener);
                    }
                }
            }

從上面可以看出,此處主要支持Dubbo服務(wù)治理平臺(tái)(dubbo-admin),平臺(tái)在啟動(dòng)時(shí)會(huì)訂閱全量接口宁否,它會(huì)感知每個(gè)服務(wù)的狀態(tài)窒升。

接下來(lái)看一下普通消費(fèi)者的訂閱邏輯,首先根據(jù)URL的類別得到一組需要訂閱的路徑慕匠。如果類別是*饱须,則會(huì)訂閱四種類型的路徑(providers、routers台谊、consumers蓉媳、configurators),否則只訂閱providers路徑锅铅,代碼如下:

    List<URL> urls = new ArrayList<>();
    //根據(jù)URL的類別督怜,獲取一組要訂閱的路徑
        for (String path : toCategoriesPath(url)) {
     //如果listeners緩存為空則創(chuàng)建緩存
      ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
      //如果zkListener緩存為空則創(chuàng)建緩存
      ChildListener zkListener = listeners.computeIfAbsent(listener, k -> new RegistryChildListenerImpl(url, path, k, latch));
      if (zkListener instanceof RegistryChildListenerImpl) {
        ((RegistryChildListenerImpl) zkListener).setLatch(latch);
      }
      zkClient.create(path, false);
      List<String> children = zkClient.addChildListener(path, zkListener);
      if (children != null) {
        //訂閱,返回該節(jié)點(diǎn)下的子路徑并緩存狠角。
        urls.addAll(toUrlsWithEmpty(url, path, children));
      }
    }
        //回調(diào)NotifyListener号杠,更新本地緩存信息
    notify(url, listener, urls);

注意:此處會(huì)根據(jù)URL中的category屬性獲取具體的類別(providers、routers丰歌、consumers姨蟋、configurators),然后拉取直接子節(jié)點(diǎn)的數(shù)據(jù)進(jìn)行通知立帖,如果是providers類別的數(shù)據(jù)眼溶,則訂閱方會(huì)更新本地Directory管理的Invoker服務(wù)列表,如果是roters分類晓勇,則訂閱會(huì)更新本地路由規(guī)則列表堂飞,如果是configuators類別,則訂閱方會(huì)更新或覆蓋本地動(dòng)態(tài)參數(shù)列表绑咱。

Nacos的實(shí)現(xiàn)

發(fā)布的實(shí)現(xiàn)

Nacos發(fā)布代碼也不復(fù)雜绰筛,只是調(diào)用Nacos的客戶端注冊(cè)了一個(gè)實(shí)例,代碼如下:

注冊(cè)實(shí)例

 public void doRegister(URL url) {
        try {
            String serviceName = getServiceName(url);
            Instance instance = createInstance(url);
            /**
             *  namingService.registerInstance with {@link org.apache.dubbo.registry.support.AbstractRegistry#registryUrl}
             *  default {@link DEFAULT_GROUP}
             *
             * in https://github.com/apache/dubbo/issues/5978
             */
             //調(diào)用nacos客戶端創(chuàng)建實(shí)例
            namingService.registerInstance(serviceName,
                getUrl().getGroup(Constants.DEFAULT_GROUP), instance);
        } catch (Throwable cause) {
            throw new RpcException("Failed to register " + url + " to nacos " + getUrl() + ", cause: " + cause.getMessage(), cause);
        }
    }

dubbo中描融,所以的服務(wù)都被封裝成了URL铝噩,對(duì)應(yīng)nacos中的服務(wù)實(shí)例Instance,所以服務(wù)注冊(cè)時(shí)窿克,只需要簡(jiǎn)單的將URL轉(zhuǎn)換成Instance就可以注冊(cè)到nacos中骏庸,針對(duì)具體細(xì)節(jié)可參考nacos官方文檔

銷毀實(shí)例

public void doUnregister(final URL url) {
    try {
        String serviceName = getServiceName(url);
        Instance instance = createInstance(url);
        //調(diào)用nacos客戶端刪除實(shí)例
        namingService.deregisterInstance(serviceName,
            getUrl().getGroup(Constants.DEFAULT_GROUP),
            instance.getIp()
            , instance.getPort());
    } catch (Throwable cause) {
        throw new RpcException("Failed to unregister " + url + " to nacos " + getUrl() + ", cause: " + cause.getMessage(), cause);
    }
}

訂閱的實(shí)現(xiàn)

org.apache.dubbo.registry.nacos.NacosRegistry:518

    private void subscribeEventListener(String serviceName, final URL url, final NotifyListener listener)
        throws NacosException {
        EventListener eventListener = new RegistryChildListenerImpl(serviceName, url, listener);
        namingService.subscribe(serviceName,
            getUrl().getGroup(Constants.DEFAULT_GROUP),
            eventListener);
    }

nacos的服務(wù)監(jiān)聽(tīng)是EventListener毛甲,所以dubbo的服務(wù)訂閱只需要將NotifyListener的處理包裝進(jìn)onEvent中處理即可, 通過(guò)namingService.subscribe添加nacos的訂閱具被。最終EventListener對(duì)象會(huì)被添加到事件調(diào)度器的監(jiān)聽(tīng)器列表中玻募,見(jiàn)如下代碼

com.alibaba.nacos.client.naming.event.InstancesChangeNotifier:54

private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();
    
    private final Object lock = new Object();
    
    /**
     * register listener.
     *
     * @param groupName   group name
     * @param serviceName serviceName
     * @param clusters    clusters, concat by ','. such as 'xxx,yyy'
     * @param listener    custom listener
     */
    public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
        String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
        ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
        if (eventListeners == null) {
            synchronized (lock) {
                eventListeners = listenerMap.get(key);
                if (eventListeners == null) {
                    eventListeners = new ConcurrentHashSet<EventListener>();
                    listenerMap.put(key, eventListeners);
                }
            }
        }
        eventListeners.add(listener);
    }

當(dāng)有instanceEvent變化時(shí)觸發(fā)他的onEvent方法,代碼如下:

@Override
public void onEvent(InstancesChangeEvent event) {
    String key = ServiceInfo
            .getKey(NamingUtils.getGroupedName(event.getServiceName(), event.getGroupName()), event.getClusters());
    ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
    if (CollectionUtils.isEmpty(eventListeners)) {
        return;
    }
    for (final EventListener listener : eventListeners) {
        final com.alibaba.nacos.api.naming.listener.Event namingEvent = transferToNamingEvent(event);
        if (listener instanceof AbstractEventListener && ((AbstractEventListener) listener).getExecutor() != null) {
            ((AbstractEventListener) listener).getExecutor().execute(() -> listener.onEvent(namingEvent));
        } else {
            listener.onEvent(namingEvent);
        }
    }
}

緩存機(jī)制

緩存的意義就在于拿空間換時(shí)間一姿,如果每次遠(yuǎn)程調(diào)用都要從注冊(cè)中心獲取一下可以調(diào)用的服務(wù)列表补箍,注冊(cè)中心要承受巨大的流量壓力。此外啸蜜,每次額外的網(wǎng)絡(luò)開銷也會(huì)讓整個(gè)系統(tǒng)的性能下降,因此Dubbo的注冊(cè)中心實(shí)現(xiàn)了通用的緩存機(jī)制辈挂,在抽象類AbstractRegistry中實(shí)現(xiàn)衬横。AbstractRegistry類結(jié)構(gòu)關(guān)系圖如下:

FailbackRegistry.png

消費(fèi)者活服務(wù)中心獲取注冊(cè)信息后會(huì)做本地緩存,內(nèi)存中會(huì)有一份终蒂,保存到Properties對(duì)象中蜂林,磁盤上也會(huì)持久化一份文件,通過(guò)file對(duì)象引用拇泣。在AbstractRegistry抽象類中有如下定義:

private final Properties properties = new Properties();
// Local disk cache file
private File file;
private final ConcurrentMap<URL, Map<String, List<URL>>> notified =
  new ConcurrentHashMap<>();

內(nèi)存中的緩存notified是ConcurrentHashMap里面又嵌套了一個(gè)Map噪叙,外層Map的key是消費(fèi)者的URL,內(nèi)層Map的key是分類霉翔,包含providers睁蕾,consumers,routers债朵,configurations四種子眶。value則是對(duì)應(yīng)的服務(wù)列表,對(duì)于沒(méi)有服務(wù)提供者提供服務(wù)的URL序芦,會(huì)已特殊的empty://前綴開頭臭杰。

緩存的加載

在服務(wù)初始化的時(shí)候,AbstractRegistry構(gòu)造函數(shù)會(huì)從本地磁盤文件中把持久化的注冊(cè)數(shù)據(jù)督導(dǎo)properties對(duì)象中谚中,并接在到內(nèi)存緩存中渴杆,代碼如下:

    private void loadProperties() {
        if (file != null && file.exists()) {
            InputStream in = null;
            try {
                //讀取磁盤中的文件
                in = new FileInputStream(file);
                properties.load(in);
                if (logger.isInfoEnabled()) {
                    logger.info("Loaded registry cache file " + file);
                }
            } catch (Throwable e) {
                logger.warn("Failed to load registry cache file " + file, e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        logger.warn(e.getMessage(), e);
                    }
                }
            }
        }
    }

Properties保存了所有服務(wù)提供者的URL,使用url#getServiceKey()作為key宪塔,提供者列表磁奖、路由規(guī)則列表、配置規(guī)則列表等作為value某筐。由于value是列表点寥,當(dāng)存在多個(gè)的時(shí)候使用空格隔開。還有一個(gè)特殊的key.registies来吩,保存所有的注冊(cè)中心的地址敢辩。如果應(yīng)用在啟動(dòng)過(guò)程中蔽莱,注冊(cè)中心無(wú)法連接活宕機(jī),則Dubbo框架會(huì)自動(dòng)通過(guò)本地緩存加載Invoker戚长。

緩存的保存與更新

緩存的保存有同步和異步兩種方式盗冷。異步會(huì)使用線程池異步保存,如果線程在執(zhí)行過(guò)程中出現(xiàn)異常同廉,則會(huì)再次調(diào)用線程池不斷重試仪糖,代碼如下所示。

同步與異步更新緩存

  if (syncSaveFile) {
  //同步保存
  doSaveProperties(version);
  } else {
  //異步保存迫肖,放入線程池锅劝,會(huì)傳入一個(gè)AtomicLong的版本號(hào),保證數(shù)據(jù)是最新的蟆湖。
  registryCacheExecutor.execute(new SaveProperties(version));
  }

AbstractRegistry#notify方法中封裝了更新內(nèi)存緩存和更新文件緩存的邏輯故爵。當(dāng)客戶端第一次訂閱獲取全量數(shù)據(jù),或者后續(xù)由于訂閱得到新數(shù)據(jù)時(shí)隅津,都會(huì)調(diào)用該方法進(jìn)行保存诬垂。

重試機(jī)制

由上面AbstractRegistry相關(guān)類關(guān)系圖我們可以得知org.apache.dubbo.registry.support.FailbackRegistry繼承了AbstractRegistry,并在此基礎(chǔ)上增加了失敗重試機(jī)制作為抽象能力伦仍。ZookeeperRegistryRedisRegistry繼承該抽象方法后结窘,直接使用即可。
FailbackRegistry抽象類中定義了一個(gè)ScheduledExecutorService,每經(jīng)過(guò)固定間隔(默認(rèn)
為5秒)調(diào)用FailbackRegistry#retry()方法充蓝。另外隧枫,該抽象類中還有五個(gè)比較重要的集合,如下表所示谓苟。

集合名稱 集合介紹
Set failedRegistered 發(fā)起注冊(cè)失敗的URL集合
Set failedUnregistered 取消注冊(cè)失敗的URL集合
ConcurrentMap> failedSubscribed 發(fā)起訂閱失敗的監(jiān)聽(tīng)器集合
ConcurrentMap> failedUnsubscribed 取消訂閱失敗的監(jiān)聽(tīng)器集合
ConcurrentMap>> failedNotified 通知失敗的URL集合

在定時(shí)器中調(diào)用retry方法的時(shí)候悠垛,會(huì)把這五個(gè)集合分別遍歷和重試,重試成功則從集合中移除娜谊。FailbackRegistry實(shí)現(xiàn)了subscribe确买、 unsubscribe 等通用方法,里面調(diào)用了未實(shí)現(xiàn)的模板方法纱皆,會(huì)由子類實(shí)現(xiàn)个曙。通用方法會(huì)調(diào)用這些模板方法闺金,如果捕獲到異常,則會(huì)把URL添加到對(duì)應(yīng)的重試集合中,以供定時(shí)器去重試啄糙。

設(shè)計(jì)模式

Dubbo注冊(cè)中心擁有良好的擴(kuò)展性铝宵,用戶可以在其基礎(chǔ)上代嗤,快速開發(fā)出符合自己業(yè)務(wù)需求的注冊(cè)中心菱农。這種擴(kuò)展性和Dubbo中使用的設(shè)計(jì)模式密不可分,下面介紹注冊(cè)中心模塊使用的設(shè)計(jì)模式。學(xué)完之后歧譬,能降低讀者對(duì)注冊(cè)中心源碼閱讀的門檻岸浑。

模板模式

整個(gè)注冊(cè)中心的邏輯部分使用了模板模式,其類的關(guān)系圖如圖所示瑰步。

ZookeeperRegistry.png

AbstractRegistry實(shí)現(xiàn)了Registry 接口中的注冊(cè)矢洲、訂閱、查詢缩焦、通知等方法读虏,還實(shí)現(xiàn)了磁盤文件持久化注冊(cè)信息這一通用方法。 但是注冊(cè)袁滥、訂閱盖桥、查詢、通知等方法只是簡(jiǎn)單地把URL加入對(duì)應(yīng)的集合题翻,沒(méi)有具體的注冊(cè)或訂閱邏輯揩徊。

FailbackRegistry又繼承了AbstractRegistry, 重寫了父類的注冊(cè)、訂閱藐握、查詢和通知等方法,并且添加了重試機(jī)制垃喊。此外猾普,還添加了四個(gè)未實(shí)現(xiàn)的抽象模板方法,代碼如下本谜。

未實(shí)現(xiàn)的抽象模板方法

        // ==== Template method ====

    public abstract void doRegister(URL url);

    public abstract void doUnregister(URL url);

    public abstract void doSubscribe(URL url, NotifyListener listener);

    public abstract void doUnsubscribe(URL url, NotifyListener listener);

以訂閱為例初家,FailbackRegistry 重寫了subscribe 方法,但只實(shí)現(xiàn)了訂閱的大體邏輯及異常處理等通用性的東西乌助。具體如何訂閱溜在,交給繼承的子類實(shí)現(xiàn)。這就是模板模式的具體實(shí)現(xiàn)他托,代碼如下所示掖肋。

模板模式調(diào)用

    public void subscribe(URL url, NotifyListener listener) {
        super.subscribe(url, listener);
        removeFailedSubscribed(url, listener);
        try {
                //此處調(diào)用了模板方法,由子類自行實(shí)現(xiàn)
            // Sending a subscription request to the server side
            doSubscribe(url, listener);
        } catch (Exception e) {
            ...
        }
    }

工廠模式

所有的注冊(cè)中心實(shí)現(xiàn)赏参,都是通過(guò)對(duì)應(yīng)的工廠創(chuàng)建的志笼。工廠類之間的關(guān)系如圖所示。

ZookeeperRegistryFactory.png

AbstractRegistryFactory實(shí)現(xiàn)了RegistryFactory 接口的getRegistry(URL ur1)方法把篓,是一個(gè)通用實(shí)現(xiàn)纫溃,主要完成了加鎖,以及調(diào)用抽象模板方法createRegistry(URL ur1)創(chuàng)建具體實(shí)現(xiàn)等操作韧掩,并緩存在內(nèi)存中紊浩。抽象模板方法會(huì)由具體子類繼承并實(shí)現(xiàn),代碼如下所示。

getRegistry抽象實(shí)現(xiàn)

        // Lock the registry access process to ensure a single instance of the registry
        registryManager.getRegistryLock().lock();
        try {
            // double check
            // fix https://github.com/apache/dubbo/issues/7265.
            defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed();
            if (null != defaultNopRegistry) {
                return defaultNopRegistry;
            }
            registry = registryManager.getRegistry(key);
            if (registry != null) {
              //緩存中有 則直接返回
                return registry;
            }
            //create registry by spi/ioc
            //如果注冊(cè)中心還沒(méi)創(chuàng)建過(guò)坊谁,
            //則調(diào)用抽象方法createRegistry(ur1)重新創(chuàng)建一個(gè)createRegistry方法由具體的子類實(shí)現(xiàn)  
            registry = createRegistry(url);
        } catch (Exception e) {
            if (check) {
                throw new RuntimeException("Can not create registry " + url, e);
            } else {
                LOGGER.warn("Failed to obtain or create registry ", e);
            }
        } finally {
            // Release the lock
            registryManager.getRegistryLock().unlock();
        }
                if (registry != null) {
            //創(chuàng)建成功 緩存起來(lái)
            registryManager.putRegistry(key, registry);
        }

雖然每種注冊(cè)中心都有自己具體的工廠類费彼,但是在什么地方判斷,應(yīng)該調(diào)用哪個(gè)工廠類實(shí)現(xiàn)呢?代碼中并沒(méi)有看到顯式的判斷呜袁。答案就在RegistryFactory 接口中敌买,該接口里有一個(gè)Registry getRegistry(URL url)方法,該方法上有@Adaptive({"protocol"})注解阶界,代碼如下所示虹钮。

RegistryFactory源碼

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

}

了解AOP的胖友就會(huì)很容易理解,這個(gè)注解會(huì)自動(dòng)生成代碼實(shí)現(xiàn)一些邏輯膘融,它的value參數(shù)會(huì)從URL中獲取protocol 鍵的值芙粱,并根據(jù)獲取的值來(lái)調(diào)用不同的工廠類。例如氧映,當(dāng)url.protocol = zookeeper獲得 ZookeeperRegistryFactory實(shí)現(xiàn)類春畔。具體Adaptive注解的實(shí)現(xiàn)原理會(huì)在第4章Dubbo加載機(jī)制中講解。

小結(jié)

本篇介紹了Dubbo中已經(jīng)支持的注冊(cè)中心岛都。重點(diǎn)介紹了ZooKeeper和Nacos兩種注冊(cè)中心律姨。講解了兩種注冊(cè)中心的數(shù)據(jù)結(jié)構(gòu),以及訂閱發(fā)布機(jī)制的具體實(shí)現(xiàn)臼疫。然后介紹了注冊(cè)中心中一些通用的關(guān)鍵特性择份,如數(shù)據(jù)緩存、重試等機(jī)制烫堤。最后荣赶,在對(duì)各種機(jī)制已經(jīng)了解的前提下,理解了整個(gè)注冊(cè)中心源碼的設(shè)計(jì)模式鸽斟。下一篇拔创,我們會(huì)詳細(xì)探討Dubbo SPI擴(kuò)展點(diǎn)加載的原理。

關(guān)注我富蓄,獲取免費(fèi)學(xué)習(xí)資料剩燥,無(wú)套路。
公眾號(hào):爪哇干貨分享

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末立倍,一起剝皮案震驚了整個(gè)濱河市躏吊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帐萎,老刑警劉巖比伏,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異疆导,居然都是意外死亡赁项,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悠菜,“玉大人舰攒,你說(shuō)我怎么就攤上這事』诖祝” “怎么了摩窃?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)芬骄。 經(jīng)常有香客問(wèn)我猾愿,道長(zhǎng),這世上最難降的妖魔是什么账阻? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任蒂秘,我火速辦了婚禮,結(jié)果婚禮上淘太,老公的妹妹穿的比我還像新娘姻僧。我一直安慰自己,他們只是感情好蒲牧,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布撇贺。 她就那樣靜靜地躺著,像睡著了一般冰抢。 火紅的嫁衣襯著肌膚如雪松嘶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天晒屎,我揣著相機(jī)與錄音喘蟆,去河邊找鬼缓升。 笑死鼓鲁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的港谊。 我是一名探鬼主播骇吭,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼歧寺!你這毒婦竟也來(lái)了燥狰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤斜筐,失蹤者是張志新(化名)和其女友劉穎龙致,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顷链,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡目代,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榛了。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡在讶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出霜大,到底是詐尸還是另有隱情构哺,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布战坤,位于F島的核電站曙强,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏湖笨。R本人自食惡果不足惜旗扑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慈省。 院中可真熱鬧臀防,春花似錦、人聲如沸边败。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)笑窜。三九已至致燥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間排截,已是汗流浹背嫌蚤。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留断傲,地道東北人脱吱。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像认罩,于是被迫代替她去往敵國(guó)和親箱蝠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 3.1 注冊(cè)中心概述 在Dubbo微服務(wù)體系中垦垂,注冊(cè)中心是其核心組件之一宦搬。Dubbo通過(guò)注冊(cè)中心實(shí)現(xiàn)了分布式環(huán)境中...
    方雲(yún)閱讀 121評(píng)論 0 2
  • 1 Dubbo注冊(cè)中心概述 Dubbo的注冊(cè)中心承擔(dān)著Dubbo服務(wù)的注冊(cè)與發(fā)現(xiàn)的功能。 Dubbo支持的注冊(cè)中心...
    愛(ài)健身的兔子閱讀 1,331評(píng)論 0 0
  • 注冊(cè)中心概述: Dubbo通過(guò)注冊(cè)中心實(shí)現(xiàn)了分布式環(huán)境的服務(wù)注冊(cè)和發(fā)現(xiàn),是分布式節(jié)點(diǎn)的紐帶劫拗,主要作用如下: 1.動(dòng)...
    柳實(shí)閱讀 471評(píng)論 0 0
  • 一.Dubbo意義 網(wǎng)站應(yīng)用的架構(gòu)變化經(jīng)歷了一個(gè)從所有服務(wù)分布在一臺(tái)服務(wù)器上(All in one 间校、單一應(yīng)用架構(gòu)...
    猿燈塔閱讀 217評(píng)論 0 0
  • Zookeeper注冊(cè)中心安裝 建議使用dubbo-2.3.3以上版本的zookeeper注冊(cè)中心客戶端。Zook...
    mingli_jianshu1閱讀 361評(píng)論 0 1