jannal(无名小宝)

没有失败,只有缓慢的成功

导航

Dubbo之注册中心

dubbo版本

  1. dubbo版本2.6.7

注册中心

  1. Dubbo通过注册中心实现了各服务之间的注册与发现
    • 动态加入:provider通过注册中心动态暴露服务给consumer
    • 动态发现:一个消费者可以动态感知新的配置、路由规则、新的consumer,不需要重启服务
    • 动态调整:注册中心支持参数的动态调整,新参数自动更新到所有相关服务节点
    • 统一配置:避免了本地配置导致每个服务的配置不一致
  2. Dubbo注册中心模块
    • dubbo-registry-api:顶层API和抽象类
    • dubbo-registry-default:基于内存的默认实现
    • dubbo-registry-multicast:multicast模式的服务注册与发现,multicast模式不需要启动任何注册中心,只要通过广播地址,就可以互相发现。provider启动时会广播自己的地址,consumer启动时会广播订阅请求,provider收到订阅请求会根据配置广播或单播给订阅者。不建议在生产环境使用
    • dubbo-registry-zookeeper:zookeeper作为注册中心的实现
    • dubbo-registry-redis:redis作为注册中心的实现
    • dubbo-registry-nacos:nacos作为注册中心的实现

直连模式

  1. 点对点的直连方式:在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者。点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。

  2. 配置方式

    1. 第一优先级:在JVM启动参数中加入-D参数映射服务地址
    java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890
    
    2. 第二优先级:在映射文件 xxx.properties 中加入配置(key 为服务名,value 为服务提供者)
    指定映射文件,2.0 以上版本自动加载 ${user.home}/dubbo-resolve.properties文件,不需要配置
    java -Ddubbo.resolve.file=xxx.properties
    映射文件内容
    com.alibaba.xxx.XxxService=dubbo://localhost:20890
    
    3. 通过xml配置,多个地址用分号隔开
    <dubbo:reference id="xxxService" 
                     interface="com.alibaba.xxx.XxxService" 
                     url="dubbo://localhost:20890" />
    
    4. 指定注册中心方式
    <dubbo:reference id="demoService"
                     interface="cn.jannal.dubbo.facade.DemoService"
                     group="group0"
                     version="1.0.0"
                     url="registry://dubbo-zookeeper:2181/cn.jannal.dubbo.facade.DemoService/?registry=zookeeper"
    />
    
  3. 加载映射文件的源码ReferenceConfig#init。先从xml配置中获取,通过ReferenceConfig#setUrl赋值。然后调用ReferenceConfig#init,先从系统属性加载,如果有,则覆盖。如果没有,则从解析文件中加载

    ...省略...
    // 从系统属性中获取解析文件路径,java -Ddubbo.resolve.file=xxx.properties 优先级高
    resolveFile = System.getProperty("dubbo.resolve.file");
    if (resolveFile == null || resolveFile.length() == 0) {
        // 从指定位置加载配置文件
        File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties");
        if (userResolveFile.exists()) {
            // 获取文件绝对路径
            resolveFile = userResolveFile.getAbsolutePath();
        }
    }
    ...省略...
    
  4. ReferenceConfig#createProxy有对于点对点直连模式的处理

    private T createProxy(Map<String, String> map) {
        ...省略...
      // 远程引用,url 不为空,表明想进行点对点调用
    	if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
        String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
        // 当需要配置多个 url 时,可用分号进行分割,这里会进行切分
        if (us != null && us.length > 0) {
            for (String u : us) {
                URL url = URL.valueOf(u);
                if (url.getPath() == null || url.getPath().length() == 0) {
                    // 设置接口全限定名为 url 路径
                    url = url.setPath(interfaceName);
                }
                // 检测 url 协议是否为 registry,若是,表明用户想使用指定的注册中心
               // xml中的url配置类似如下格式registry://dubbo-zookeeper:2181/cn.jannal.dubbo.facade.DemoService/?registry=zookeeper
                if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                    // 将 map 转换为查询字符串,并作为 refer 参数的值添加到 url 中
                    urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                } else {
                    // 合并 url,移除服务提供者的一些配置(这些配置来源于用户配置的 url 属性),
                    // 比如线程池相关配置。并保留服务提供者的部分配置,比如版本,group,时间戳等
                    // 最后将合并后的配置设置为 url 查询字符串中。
                    urls.add(ClusterUtils.mergeUrl(url, map));
                }
            }
        }
    	} 
      ...省略...
      if (urls.size() == 1) {
        // 调用 RegistryProtocol 的 refer 构建 Invoker 实例
        invoker = refprotocol.refer(interfaceClass, urls.get(0));
      }
      ...省略...
    }
    

zookeeper注册中心

  1. 官方文档流程图

  2. 流程

    • providers启动时,向 /dubbo/com.foo.BarService/providers 目录下URL地址
    • consumer者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下自己的URL地址
    • 监控中心启动时: 订阅 /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。
  3. 树的根(Root)节点是注册中心分组,可通过如下方式设置根节点,默认是/dubbo

    <dubbo:registry group="dubbo" /> 
    
  4. 服务接口(Service)下包含4个子目录providers、consumers、routers、configurations

    • /dubbo/Service/providers:多个providers URL元数据信息,持久节点
    • /dubbo/Service/consumers:多个consumers URL元数据信息,持久节点
    • /dubbo/Service/routers:多个consumer路由策略URL元数据信息
    • /dubbo/Service/configurators:多个providers动态配置 URL元数据信息
  5. 运行dubbo-demo-provider程序查看zookeeper信息

  6. URL节点为临时节点,URL解码后的数据为

    dubbo://192.168.1.3:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=com.alibaba.dubbo.demo.DemoService&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=59124&side=provider&timestamp=1577191564974
    
  7. 启动dubbo-demo-consumer程序查看zookeeper信息

  8. URL节点为临时节点,URL解码后的数据为

    consumer://10.37.129.2/com.alibaba.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=62143&qos.port=33333&side=consumer&timestamp=1577195067440
    
  9. Zookeeper注册中心采用事件通知和客户端拉取的方式

    • 客户端在第一次连接注册中心后,会获取对应目录下的全量数据。并在订阅的节点上注册一个watcher。后续如果节点有数据变化,会回调通知客户端(事件通知)。客户端收到通知后,会把对应节点下的全量数据都拉取过来(客户端主动拉取)。全局拉取的缺陷是,当节点数太多时,对网络会造成很大压力

相关配置

  1. 记录失败注册和订阅请求,后台定时重试

    <dubbo:registry check="false" />
    
  2. 设置 zookeeper 登录信息

    <dubbo:registry username="admin" password="1234" />
    
  3. 设置 zookeeper 的根节点,不配置默认使用/dubbo

    <dubbo:registry group="dubbo" />
    
    2. 同一个zookeeper用于多组注册中心
    <dubbo:registry id="group1Registry" protocol="zookeeper" address="zk:2181" group="group1" />
    <dubbo:registry id="group2Registry" protocol="zookeeper" address="zk:2181" group="group2" />
    
  4. 订阅服务的所有分组和所有版本的提供者

    <dubbo:reference group="*" version="*" />
    
  5. 使用curator实现

    <dubbo:registry  client="curator" />
    或者
    zookeeper://10.20.153.10:2181?client=curator
    或者
    dubbo.registry.client=curator
    
    增加依赖包
    <dependency>
        <groupId>com.netflix.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>1.1.10</version>
    </dependency>
    
  6. zookeeper地址配置

    1. 单机配置
    <dubbo:registry address="zookeeper://zookeeper:2181" />
    或者
    <dubbo:registry protocol="zookeeper" address="zookeeper:2181" />
      
    2. 集群配置  
    <dubbo:registry address="zookeeper://zookeeper1:2181?backup=zookeeper2:2181,zookeeper3:2181" />  
    或者
    <dubbo:registry protocol="zookeeper" address="zookeeper1:2181,zookeeper2:2181,zookeeper3:2181" />  
      
    3. 多个集群多个节点,使用|分隔
    <dubbo:registry protocol="zookeeper" address="zookeeper1:2181|zookeeper2:2181|zookeeper3:2181" />    
    

多注册中心

  1. Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。

  2. 多注册中心注册:同一个服务需要注册到不同机房的不同注册中心下或者同机房不同的注册中心下

    <!-- 多注册中心配置 -->
    <dubbo:registry id="hangzhouRegistry" address="zk1:2181" />
    <dubbo:registry id="qingdaoRegistry" address="zk2:2181" default="false" />
    <!-- 向多个注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService"
                   version="1.0.0" ref="helloService"
                   registry="hangzhouRegistry,qingdaoRegistry" />
    
  3. 不同服务使用不同注册中心:有些服务接口时为了给中文站使用,有的是给国际站使用

    <dubbo:registry id="chinaRegistry" address="zk1:2181" />
    <dubbo:registry id="intlRegistry" address="zk2:2181" default="false" />
    <!-- 向中文站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService"
                   version="1.0.0" 
                   ref="helloService" 
                   registry="chinaRegistry" />
    <!-- 向国际站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" 
                   version="1.0.0"
                   ref="demoService" 
                   registry="intlRegistry" />
    
  4. 多注册中心:同一个接口在不同的机房部署,接口名称和版本号一样,但是数据库不一样

    <!-- 多注册中心配置 -->
    <dubbo:registry id="chinaRegistry" address="zk1:2181" />
    <dubbo:registry id="intlRegistry" address="zk2:2181" default="false" />
    <!-- 引用中文站服务 -->
    <dubbo:reference id="chinaHelloService" interface="com.alibaba.hello.api.HelloService"
                     version="1.0.0" registry="chinaRegistry" />
    <!-- 引用国际站站服务 -->
    <dubbo:reference id="intlHelloService" interface="com.alibaba.hello.api.HelloService" 
                     version="1.0.0" registry="intlRegistry" />
    
  5. 简化配置:连接两个不同的注册中心,使用竖号分隔多个不同注册中心地址

    <!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
    <dubbo:registry address="zk1:2181|zk2:2181" />
    <!-- 引用服务 -->
    <dubbo:reference id="helloService"
                     interface="com.alibaba.hello.api.HelloService"
                     version="1.0.0" />
    

只订阅不注册

  1. 如果一个正在开发中的服务提供注册,可能会影响消费者不能正常运行。可以让服务提供者只订阅服务,而不注册服务(只获取服务,不发布服务

  2. 配置

    <dubbo:registry address="dubbo-zookeeper:2181" register="false" />
    或者
    <dubbo:registry address="dubbo-zookeeper:2181?register=false" />
    

只注册不订阅

  1. 一个应用既需要作为服务提供者注册服务,又需要作为服务消费者订阅服务。但在开发阶段,服务提供者正在开发调试,不能订阅该服务,但是本应用作为服务提供者不能停止,所以需要做到只注册而不订阅服务。配置如下:

    <dubbo:registry id="qdRegistry" address="dubbo-zookeeper:2181" subscribe="false" />
    或者
    <dubbo:registry id="qdRegistry" address="dubbo-zookeeper:2181?subscribe=false" />
    

源码位置

  1. AbstractInterfaceConfig#loadRegistries根据register和subscribe的配置,是否添加到注册列表中

    protected List<URL> loadRegistries(boolean provider) {
      ...省略...
       // 检测是否存在注册中心配置类,不存在则抛出异常
       //address 可能包含多个注册中心 ip
       List<URL> urls = UrlUtils.parseURLs(address, map);
       for (URL url : urls) {
           url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
           // 将 URL 协议头设置为 registry
           url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
           //是否添加 url 到 registryList 中
           // (服务提供者 && register = true 或 null) || (非服务提供者 && subscribe = true 或 null)
           if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                   || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
               registryList.add(url);
           }
       }
       ...省略...
       return registryList;
    }
    

源码分析(Zookeeper为例)

  1. UML图

  2. 接口说明

    //Node是节点的抽象,表示 Provider、Consumer节点以及注册中心节点
    public interface Node {
        //获取当前节点的 URL
        URL getUrl();
      	//检测当前节点是否可用
        boolean isAvailable();
      	//销毁当前节点,释放资源
        void destroy();
    }
    
    //注册中心服务接口,抽象注册服务中心的行为
    public interface RegistryService {
    
        /**
         * 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则,等数据。
         * 注册需处理契约
         * 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。<br>
         * 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。<br>
         * 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。<br>
         * 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。<br>
         * 5. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
         * @param url Registration information , is not allowed to be empty, e.g: dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
         */
        void register(URL url);
    
        /**
         * 取消注册需处理契约:
         * 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。<br>
         * 2. 按全URL匹配取消注册。<br>
         * @param url Registration information , is not allowed to be empty, e.g: dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
         */
        void unregister(URL url);
    
        /**
         * 订阅符合条件的已注册数据,当有注册数据变更时自动推送.
         * 订阅需处理契约:<br>
         * 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。<br>
         * 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。<br>
         * 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0<br>
         * 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*<br>
         * 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。<br>
         * 6. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
         * 7. 必须阻塞订阅过程,等第一次通知完后再返回。<br>
         *
         * 在 URL的category 属性上,表示订阅的数据分类。目前有四种类型:
         * 1 consumers 服务消费者列表
         * 2 providers 服务提供者列表
         * 3 routers 路由规则列表
         * 4 configurations 配置规则列表
         */
        void subscribe(URL url, NotifyListener listener);
    
        /**
         * 取消订阅需处理契约:<br>
         * 1. 如果没有订阅,直接忽略。<br>
         * 2. 按全URL匹配取消订阅。<br>
         */
        void unsubscribe(URL url, NotifyListener listener);
    
        /**
         * 查询符合条件的已注册数据,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
         */
        List<URL> lookup(URL url);
    
    }
    
    //注册中心接口,没有任何方法,只是继承方法
    public interface Registry extends Node, RegistryService {
    }
    
  3. AbstracrRegistry实现了Registry接口中的注册、订阅、查询、通知等方法,还实现了磁盘文件持久化注册信息这一通用方法

    // Local disk cache, where the special key value.registies records the list of registry centers, and the others are the  list of notified service providers
    //本地磁盘缓存,有一个特殊的key值为registies,记录的是注册中心列表,其他记录的都是服务提供者列表
    private final Properties properties = new Properties();
    // File cache timing writing
    private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
    // Is it synchronized to save the file
    //是否同步保存文件
    private final boolean syncSaveFile;
    //上次缓存更新的版本号
    private final AtomicLong lastCacheChanged = new AtomicLong();
    //已经注册的URL集合
    private final Set<URL> registered = new ConcurrentHashSet<URL>();
    //订阅的监听器集合
    private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    // 某个消费者 被通知的 服务URL 集合
    // 内层Map的可以是category类别,包含providers、consumers、routes、configurators
    private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();
    //注册中心URL
    private URL registryUrl;
    // Local disk cache file
    //本地磁盘缓存文件,用于缓存注册中心的数据
    private File file;
    
  4. FailbackRegistry主要用来做失败重试操作(包括:注册失败/反注册失败/订阅失败/反订阅失败/通知失败的重试);也提供了供ZookeeperRegistry使用的zk重连后的恢复工作的方法。

    public abstract class FailbackRegistry extends AbstractRegistry {
    
        // Scheduled executor service
        private final ScheduledExecutorService retryExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryFailedRetryTimer", true));
    
        // Timer for failure retry, regular check if there is a request for failure, and if there is, an unlimited retry
        private final ScheduledFuture<?> retryFuture;
        //发起注册失败的URL集合
        private final Set<URL> failedRegistered = new ConcurrentHashSet<URL>();
        //取消注册失败的URL集合
        private final Set<URL> failedUnregistered = new ConcurrentHashSet<URL>();
        //发起订阅失败的监听器集合
        private final ConcurrentMap<URL, Set<NotifyListener>> failedSubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
        //取消订阅失败的监听器集合
        private final ConcurrentMap<URL, Set<NotifyListener>> failedUnsubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
        //通知失败的URL集合
        private final ConcurrentMap<URL, Map<NotifyListener, List<URL>>> failedNotified = new ConcurrentHashMap<URL, Map<NotifyListener, List<URL>>>();
    		...省略...
    }
    

注册

  1. 加载注册配置中心的核心业务逻辑AbstractInterfaceConfig#loadRegistries

    • 监测注册中心配置,优先从系统属性dubbo.registry.address加载地址
    • 构造参数Map,后续转换为URL
    • 将 URL 协议头设置为 registry,添加到注册列表 registryList 中
    protected List<URL> loadRegistries(boolean provider) {
        // 检测是否存在注册中心配置类,不存在则抛出异常
        checkRegistry();
        List<URL> registryList = new ArrayList<URL>();
        if (registries != null && !registries.isEmpty()) {
            for (RegistryConfig config : registries) {
                String address = config.getAddress();
                if (address == null || address.length() == 0) {
                    // 若 address 为空,则将其设为 0.0.0.0
                    address = Constants.ANYHOST_VALUE;
                }
                // 从系统属性中加载注册中心地址
                String sysaddress = System.getProperty("dubbo.registry.address");
                if (sysaddress != null && sysaddress.length() > 0) {
                    address = sysaddress;
                }
                // 检测 address 是否合法,address=N/A表示由 dubbo 自动分配地址(直连方式,不通过注册中心)
                if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                    Map<String, String> map = new HashMap<String, String>();
                    // 添加 ApplicationConfig 中的字段信息到 map 中
                    appendParameters(map, application);
                    // 添加 RegistryConfig 字段信息到 map 中
                    appendParameters(map, config);
                    map.put("path", RegistryService.class.getName());
                    map.put("dubbo", Version.getProtocolVersion());
                    map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
                    if (ConfigUtils.getPid() > 0) {
                        map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
                    }
                    if (!map.containsKey("protocol")) {
                        if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
                            map.put("protocol", "remote");
                        } else {
                            map.put("protocol", "dubbo");
                        }
                    }
                    //address 可能包含多个注册中心 ip
                    List<URL> urls = UrlUtils.parseURLs(address, map);
                    for (URL url : urls) {
                        url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
                        // 将 URL 协议头设置为 registry
                        url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
                        //是否添加 url 到 registryList 中
                        // (服务提供者 && register = true 或 null) || (非服务提供者 && subscribe = true 或 null)
                        if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                                || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
                            registryList.add(url);
                        }
                    }
                }
            }
        }
        return registryList;
    }
    
  2. RegistryProtocol#export中会调用RegistryProtocol#register注册元数据到注册中心。通过SPI获取ZookeeperRegistryFactory,调用ZookeeperRegistry#register实现注册

    public void register(URL registryUrl, URL registedProviderUrl) {
        Registry registry = registryFactory.getRegistry(registryUrl);
        registry.register(registedProviderUrl);
    }
    
  3. register方法来自于AbstractRegistry#register,主要是校验URL,并将URL添加到【已经注册的URL集合】中。

    public void register(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("register url == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Register: " + url);
        }
      	//已经注册的URL集合
        registered.add(url);
    }  
    
  4. FailbackRegistry#register模板注册方法

    @Override
    public void register(URL url) {
        super.register(url);
        //从失败的集合中移除URL
        failedRegistered.remove(url);
        failedUnregistered.remove(url);
        try {
            // Sending a registration request to the server side
            doRegister(url);
        } catch (Exception e) {
            Throwable t = e;
    
            // If the startup detection is opened, the Exception is thrown directly.
            //如果开启了自动检测(check=true),则直接抛出异常
            boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                    && url.getParameter(Constants.CHECK_KEY, true)
                    && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
            boolean skipFailback = t instanceof SkipFailbackWrapperException;
            if (check || skipFailback) {
                if (skipFailback) {
                    t = t.getCause();
                }
                throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
            } else {
                logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
            }
    
            // Record a failed registration request to a failed list, retry regularly
            failedRegistered.add(url);
        }
    }
    
  5. ZookeeperRegistry#doRegister:默认创建持久节点。持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点,不会因为创建该节点的客户端会话失效而消失。临时节点的生命周期和客户端会话绑定,客户端会话失效,那么这个节点就会自动被清除掉

    @Override
    protected void doRegister(URL url) {
        try {
          	//toUrlPath: /dubbo/com.alibaba.dubbo.demo.DemoService/providers/ + URL
            //默认dynamic=true,表示创建持久节点
            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);
        }
    }
    

订阅

  1. AbstractRegistry#subscribe只是做了NotifyListener的保存

    @Override
    public void subscribe(URL url, NotifyListener listener) {
        if (url == null) {
            throw new IllegalArgumentException("subscribe url == null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("subscribe listener == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Subscribe: " + url);
        }
        Set<NotifyListener> listeners = subscribed.get(url);
        if (listeners == null) {
            subscribed.putIfAbsent(url, new ConcurrentHashSet<NotifyListener>());
            listeners = subscribed.get(url);
        }
        listeners.add(listener);
    }
    
  2. FailbackRegistry#subscribe是一个模板方法

    @Override
    public void subscribe(URL url, NotifyListener listener) {
        super.subscribe(url, listener);
        removeFailedSubscribed(url, listener);
        try {
            // Sending a subscription request to the server side
            //
            doSubscribe(url, listener);
        } catch (Exception e) {
            Throwable t = e;
    
            List<URL> urls = getCacheUrls(url);
            if (urls != null && !urls.isEmpty()) {
                notify(url, listener, urls);
                logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
            } else {
                // If the startup detection is opened, the Exception is thrown directly.
                boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                        && url.getParameter(Constants.CHECK_KEY, true);
                boolean skipFailback = t instanceof SkipFailbackWrapperException;
                if (check || skipFailback) {
                    if (skipFailback) {
                        t = t.getCause();
                    }
                    throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
                } else {
                    logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
                }
            }
    
            // Record a failed registration request to a failed list, retry regularly
            addFailedSubscribed(url, listener);
        }
    }
    
  3. ZookeeperRegistry#doSubscribe是订阅的核心逻辑

    @Override
    protected void doSubscribe(final URL url, final NotifyListener listener) {
        try {
            //如果是通配符*,表示全量订阅(订阅所有),即providers、routers、consumers、configurators四种类型
            if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
                String root = toRootPath();
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                if (listeners == null) {
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                    listeners = zkListeners.get(url);
                }
                ChildListener zkListener = listeners.get(listener);
                if (zkListener == null) {
                    // 第一次创建
                    listeners.putIfAbsent(listener, new ChildListener() {
                        //变更通知回调
                        @Override
                        public void childChanged(String parentPath, List<String> currentChilds) {
                            //遍历所有子节点
                            for (String child : currentChilds) {
                                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);
                                }
                            }
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                //创建持久节点
                zkClient.create(root, false);
                List<String> services = zkClient.addChildListener(root, zkListener);
                if (services != null && !services.isEmpty()) {
                    //遍历所有服务接口的子节点,进行订阅
                    for (String service : services) {
                        service = URL.decode(service);
                        anyServices.add(service);
                        //订阅当前节点,并返回该节点下所有子节点的列表
                        subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
                                Constants.CHECK_KEY, String.valueOf(false)), listener);
                    }
                }
            } else {
                List<URL> urls = new ArrayList<URL>();
                //通过category获取类别(providers、routers、consumers、configurators)
                // 将url转变成
                //  /dubbo/xxx.DemoService/providers
                //  /dubbo/xxx.DemoService/configurators
                //  /dubbo/xxx.DemoService/routers
                // 根据url类别获取一组要订阅的路径
                for (String path : toCategoriesPath(url)) {
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                    // 如果缓存没有,则添加到缓存中
                    if (listeners == null) {
                        zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                        listeners = zkListeners.get(url);
                    }
                    ChildListener zkListener = listeners.get(listener);
                    // 同样如果监听器缓存中没有 则放入缓存
                    if (zkListener == null) {
                        listeners.putIfAbsent(listener, new ChildListener() {
                            @Override
                            public void childChanged(String parentPath, List<String> currentChilds) {
                                // 通知节点变化
                                ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
                            }
                        });
                        zkListener = listeners.get(listener);
                    }
                    zkClient.create(path, false);
                    // 订阅并返回该节点下的子路径并缓存
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                        // 有子节点组装,没有那么就将消费者的协议变成empty作为url。
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
                // 回调NotifyListener,更新本地缓存
                notify(url, listener, urls);
            }
        } catch (Throwable e) {
            throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }
    

posted on 2021-12-25 22:48  jannal  阅读(324)  评论(0)    收藏  举报