Dubbo3ソースコード第10条-登録センター(ZookeeperおよびCuratorZookeeperClient)

github.com/hsfxuebaoに注目し てください。お役に立て ば幸いです。可能だと思われる場合は、[スター]をクリックしてください。

1.レジストリAPI

1.1レジストリモジュールのプレビュー

v3バージョンでは、レジストリモジュールはサブプロジェクトの形式で存在します。まず、dubbo-registryサブプロジェクトのディレクトリを見てみましょう。image.png

dubbo-registry-apiモジュールは、主にレジストリとレジストリファクトリのインターフェイスを定義し、次にRegistryDirectoryクラスとRegistryProtocolクラス、およびいくつかの周辺サポートを定義します。

1.2コアインターフェイスの概要

1.2.1 RegistryService

public interface RegistryService {
   
    void register(URL url);
   
    void unregister(URL url);
   
    void subscribe(URL url, NotifyListener listener);
  
    void unsubscribe(URL url, NotifyListener listener);
    
    List<URL> lookup(URL url);

}

RegistryServiceレジストリサービスインターフェイスは主にであることがわかります抽象了注册,下线,订阅,取消订阅方法。次にルックアップメソッドがあります。このメソッドは、提供されたURLに従ってすべての登録済みURLを取得します。

1.2.2 RegistryFactory

レジストリファクトリインターフェイス。dubbospi拡張テクノロジに基づいてサブクラスを動的に検索できます。デフォルトの実装はDubboRegistryFactoryです。

@SPI("dubbo")
public interface RegistryFactory {

    @Adaptive({"protocol"})
    Registry getRegistry(URL url);

}

レジストリセンターを取得するための抽象メソッドがあります。このメソッドは、URLのプロトコルプロパティの値に応じて、RegistryFactoryの実装クラスに適応できます。

1.2.3レジストリ

このレジストリはレジストリのインターフェイスクラスであり、その継承関係を確認できます

public interface Registry extends Node, RegistryService {
    default int getDelay() {
        return getUrl().getParameter(REGISTRY_DELAY_NOTIFICATION_KEY, 5000);
    }

    default void reExportRegister(URL url) {
        register(url);
    }

    default void reExportUnregister(URL url) {
        unregister(url);
    }
}

ノードを継承してRegistryServiceを実装します。このRegistryServiceは、上記のセクションで説明したように、サブスクリプション、サブスクリプション解除、登録、オフラインなどの機能を定義します。このノードは実際にはノードを意味します。その定義から、比較的単純であることがわかります。 URLを取得し、使用可能かどうかにかかわらず、これら3つの関数を破棄します。

public interface Node {
    /**
     * 获取url
     * @return url.
     */
    URL getUrl();
    /**
     * 是否可用
     * @return available.
     */
    boolean isAvailable();
    /**
     * 销毁
     */
    void destroy();
}

1.2.4 NotifyListener

public interface NotifyListener {

    void notify(List<URL> urls);

    default void addServiceListener(ServiceInstancesChangedListener instanceListener) {
    }

    default URL getConsumerUrl() {
        return null;
    }

}

NotifyListener 是一个通知接口,抽象了notify通知方法,当我们向注册中心订阅某个url的时候,需要带上NotifyListener 的实现类,一旦订阅的url发生了变化就会调用NotifyListener的notify进行通知。之前我们遇到过的RegistryDirectory类有个subscribe方法,同时他自己也实现了NotifyListener接口,重写notify方法,向注册中心订阅,然后那个NotifyListener 就是它自己。

image.png

1.3 在服务暴露与服务引用是怎样使用

1.3.1 服务暴露时使用

之前我们在将服务远程暴露的时候,走的export方法,然后在RegistryProtocol的Protocol实现类中,服务提供者向注册中心进行注册

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    ...
   
    // url to registry  todo 获取注册中心实例
    final Registry registry = getRegistry(registryUrl);

    // decide if we need to delay publish
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 注册到注册中心
        register(registry, registeredProviderUrl);
    }

    // register stated url on provider model
    registerStatedUrl(registryUrl, registeredProviderUrl, register);
}

再看一下register方法

private void register(Registry registry, URL registeredProviderUrl) {
    registry.register(registeredProviderUrl);
}

register是从getRegistry(registryUrl)获取的:

protected Registry getRegistry(final URL registryUrl) {
    return registryFactory.getRegistry(registryUrl);
}

RegistryFactory注册中心工厂获取对应的Registry注册中心,然后调用register进行调用。

1.3.2 服务引用时使用

我们服务引用在Protocol层是走的refer方法,看下RegistryProtocol的refer方法是怎样用的

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    url = getRegistryUrl(url);
    // 获取registry对象
    Registry registry = registryFactory.getRegistry(url);
    ...
    // 获取cluster属性,即集群容错策略,默认failover策略
    Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
    return doRefer(cluster, registry, type, url, qs);  // 继续订阅
}

先是使用注册中心工厂获取对应的注册中心Registry对象,接着调用了doRefer方法,然后这块调用链路很长,可以参考服务订阅(本地和远程引用)源码分析,直接来到: org.apache.dubbo.registry.integration.RegistryProtocol#doCreateInvoker

protected <T> ClusterInvoker<T> doCreateInvoker(DynamicDirectory<T> directory, Cluster cluster, Registry registry, Class<T> type) {
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    // all attributes of REFER_KEY
    Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
    URL urlToRegistry = new ServiceConfigURL(
        parameters.get(PROTOCOL_KEY) == null ? DUBBO : parameters.get(PROTOCOL_KEY),
        parameters.remove(REGISTER_IP_KEY), 0, getPath(parameters, type), parameters);
    if (directory.isShouldRegister()) {
        directory.setRegisteredConsumerUrl(urlToRegistry);
        // 将当前consumer注册到注册中心
        registry.register(directory.getRegisteredConsumerUrl());
    }
    // 将所有RouterFactory激活扩展类创建的router添加到directory
    directory.buildRouterChain(urlToRegistry);
    // 服务订阅
    directory.subscribe(toSubscribeUrl(urlToRegistry));
    // 将多个invoker伪装为一个具有复合功能的invoker
    return (ClusterInvoker<T>) cluster.join(directory);
}

我们可以看到,先向注册中心注册,后又调用subscribe订阅了某个url。

2. 核心抽象类

2.1 AbstractRegistryFactory

AbstractRegistryFactory 是注册中心工厂RegistryFactory接口的抽象实现,首先看下它的class定义:

public abstract class AbstractRegistryFactory implements RegistryFactory {...}

这个没啥好说的实现RegistryFactory接口,接下来看下静态成员与成员变量

// The lock for the acquisition process of the registry
// 在创建 注册中心对象或者 销毁注册中心对象的时候会用到 锁
protected static final ReentrantLock LOCK = new ReentrantLock();

// Registry Collection Map<RegistryAddress, Registry>
// 缓存 注册中心对象使用  key就是注册中心url的一个toServiceString, value是注册中心对象
protected static final Map<String, Registry> REGISTRIES = new HashMap<>();

private static final AtomicBoolean destroyed = new AtomicBoolean(false);

接下来看下getRegistry方法实现:

@Override
public Registry getRegistry(URL url) {

    Registry defaultNopRegistry = getDefaultNopRegistryIfDestroyed();
    if (null != defaultNopRegistry) {
        return defaultNopRegistry;
    }

    url = URLBuilder.from(url)
            .setPath(RegistryService.class.getName())
            .addParameter(INTERFACE_KEY, RegistryService.class.getName())
            .removeParameters(EXPORT_KEY, REFER_KEY, TIMESTAMP_KEY)
            .build();
    String key = createRegistryCacheKey(url);
    // Lock the registry access process to ensure a single instance of the registry
    LOCK.lock();
    try {
        // double check
        // fix https://github.com/apache/dubbo/issues/7265.
        defaultNopRegistry = getDefaultNopRegistryIfDestroyed();
        if (null != defaultNopRegistry) {
            return defaultNopRegistry;
        }
        // 从缓存获取当前服务注册中心URL对应的实例。
        // key为zookeeper://zookeeperOS:2181/org.apache.dubbo.registry.RegistryService
        Registry registry = REGISTRIES.get(key);
        if (registry != null) {
            return registry;
        }
        //create registry by spi/ioc
        // 创建注册中心实例 创建方法由子类实现  模板方法设计模式
        registry = createRegistry(url);
        if (registry == null) {
            throw new IllegalStateException("Can not create registry " + url);
        }
        //将 registry添加到缓存中去
        REGISTRIES.put(key, registry);
        return registry;
    } finally {
        // Release the lock
        LOCK.unlock();
    }
}

在getRegistry方法中,前几行操作url的代码主要是为了生成一个key,类似zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService,接着获取锁,先从缓存REGISTRIES中获取该key 的value(注册中心对象),如果能获取到就返回,如果获取不到就调用抽象方法createRegistry来创建注册中心对象,这个createRegistry 方法是个抽象方法,需要子类来具体实现,先来看下createRegistry方法定义:

 // 由子类实现的 创建方法, 模板方法设计模式
protected abstract Registry createRegistry(URL url);

创建完的注册中心还是空的话抛出异常,不是空就缓存到REGISTRIES map中,最后就是释放锁了。 接下来再看下AbstractRegistryFactory其他方法:

/**
* Get all registries
* 获取所有的registry
* @return all registries
*/
public static Collection<Registry> getRegistries() {
    return Collections.unmodifiableCollection(REGISTRIES.values());
}

这个getRegistries方法没什么好说的,就是获取缓存中的所有注册中心

public static void destroyAll() {
  if (LOGGER.isInfoEnabled()) {
      LOGGER.info("Close all registries " + getRegistries());
  }
  // 循环注销各个registry  , 最后再清空registry缓存
  LOCK.lock();
  try {
      for (Registry registry : getRegistries()) {
          try {
              registry.destroy();
          } catch (Throwable e) {
              LOGGER.error(e.getMessage(), e);
          }
      }
      REGISTRIES.clear();
  } finally {
      // Release the lock
      LOCK.unlock();
  }
}

再来看下destroyAll这个静态方法,先是获取锁,遍历缓存中的所有注册中心对象,调用对应注册中心对象的destroy方法来销毁,最后释放锁。

2.2 AbstractRegistry

AbstractRegistry类是一个抽象类,是Registry 接口的抽象实现,我们先来看下class定义:

public abstract class AbstractRegistry implements Registry {...}

接下来再来看下它的构造:

public AbstractRegistry(URL url) {
    // 设置 注册 url
    setUrl(url);
    localCacheEnabled = url.getParameter(REGISTRY_LOCAL_FILE_CACHE_ENABLED, true);
    if (localCacheEnabled) {
        // Start file save timer  是否同步保存到文件中
        syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
        String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getApplication() + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
        /// 生成文件名
        String filename = url.getParameter(FILE_KEY, defaultFilename);
        File file = null;
        if (ConfigUtils.isNotEmpty(filename)) {
            file = new File(filename);
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {
                    throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
                }
            }
        }
        this.file = file;
        // When starting the subscription center,
        // we need to read the local cache file for future Registry fault tolerance processing.
        // todo
        loadProperties();
        // todo
        notify(url.getBackupUrls());
    }
}

先是调用setUrl方法设置注册中心url,接着就是获取save.file参数值,该参数值表示是否同步到文件中,缺省是false, 再往下就是获取file属性值,这个属性值就是注册中心缓存文件的位置,缺省的话就是在用户家目录/.dubbo文件夹下面 “dubbo-registry-应用名(也就application)-注册中心ip:port.cache ” 文件名,例如:

/Users/mac/.dubbo/dubbo-registry-dubbo-consumer-127.0.0.1:2181.cache

我们可以看下具体内容是什么

image.png 接着就是判断这个文件是否存在了,创建file,将file赋值给成员变量的file。
在往后调用loadProperties();加载这个文件里面的键值对了。我们看下这个方法

// 将cache 文件流load到properties中
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对象,这个很简单。
接着构造方法往后看,notify(url.getBackupUrls());这个就是从注册中心url中找到backup属性值,这个backup属性值就是需要恢复的一些服务提供者url,再调用notify进行通知,将那些恢复的url通知给订阅方,我们看下notify方法

protected void notify(List<URL> urls) {
    if (CollectionUtils.isEmpty(urls)) {
        return;
    }

    // getSubscribed() 获取所有服务订阅者
    for (Map.Entry<URL, Set<NotifyListener>> entry : getSubscribed().entrySet()) {
        URL url = entry.getKey();

        if (!UrlUtils.isMatch(url, urls.get(0))) {
            continue;
        }

        Set<NotifyListener> listeners = entry.getValue();
        if (listeners != null) {
            // 循坏通知
            for (NotifyListener listener : listeners) {
                try {
                    notify(url, listener, filterEmpty(url, urls));
                } catch (Throwable t) {
                    logger.error("Failed to notify registry event, urls: " + urls + ", cause: " + t.getMessage(), t);
                }
            }
        }
    }
}

到这咱们的构造方法就解析完成了
接下来看下它的成员有哪些:

// Local disk cache, where the special key value.registries records the list of registry centers, and the others are the list of notified service providers
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 是否同步保存到file中
private boolean syncSaveFile;
private final AtomicLong lastCacheChanged = new AtomicLong();
private final AtomicInteger savePropertiesRetryTimes = new AtomicInteger();
// 已经注册的
private final Set<URL> registered = new ConcurrentHashSet<>();
// 已经订阅的
private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<>();
// 已经通知的
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<>();
// 注册中心url
private URL registryUrl;
// Local disk cache file
private File file;
private boolean localCacheEnabled;
  • properties这个成员就是咱们loadProperties方法那个存储注册中心缓存的。
  • registryCacheExecutor这个成员是进行注册中心缓存的线程。
  • syncSaveFile 是否同步保存到文件。
  • lastCacheChanged 记录最后一次缓存的版本
  • registered 存放已经注册的url
  • subscribed 存放订阅的信息,key是订阅的url,然后value就是那些订阅者,一旦key这个url有变化,就通知

value这堆订阅者

  • notified存放已经通知的信息

接下来看下register注册方法:

@Override
public void register(URL url) {
    if (url == null) {
        throw new IllegalArgumentException("register url == null");
    }
    if (url.getPort() != 0) {
        if (logger.isInfoEnabled()) {
            logger.info("Register: " + url);
        }
    }
    registered.add(url);
}

可以看到就是检查注册url,然后添加到registered 这个set里面缓存起来。
接着看下unregister取消注册方法:

@Override
public void unregister(URL url) {
    if (url == null) {
        throw new IllegalArgumentException("unregister url == null");
    }
    if (url.getPort() != 0) {
        if (logger.isInfoEnabled()) {
            logger.info("Unregister: " + url);
        }
    }
    registered.remove(url);
}

就是检查注册url是否是空,然后从registered 这个set移除掉。
接着看下subscribe 订阅方法的实现:

@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);
    }
    // 缓存订阅的url和Listener
    Set<NotifyListener> listeners = subscribed.computeIfAbsent(url, n -> new ConcurrentHashSet<>());
    listeners.add(listener);
}

可以看到一开始检查订阅url与listener,接着就是根据订阅url获取subscribed 中的监听集合,如果没有就新建塞进去,最后将listener添加到监听集合中。
接着看下unsubscribe取消订阅的实现:

@Override
public void unsubscribe(URL url, NotifyListener listener) {
    if (url == null) {
        throw new IllegalArgumentException("unsubscribe url == null");
    }
    if (listener == null) {
        throw new IllegalArgumentException("unsubscribe listener == null");
    }
    if (logger.isInfoEnabled()) {
        logger.info("Unsubscribe: " + url);
    }
    // 移除缓存中某个url的listener
    Set<NotifyListener> listeners = subscribed.get(url);
    if (listeners != null) {
        listeners.remove(listener);
    }

    // do not forget remove notified
    notified.remove(url);
}

就是从监听集合移除这个listener
接着我们再来看下其他方法:
先来看下recover 恢复方法(该方法在子类重连接的时候会被用到)

protected void recover() throws Exception {
    // register
    Set<URL> recoverRegistered = new HashSet<>(getRegistered());
    if (!recoverRegistered.isEmpty()) {
        if (logger.isInfoEnabled()) {
            logger.info("Recover register url " + recoverRegistered);
        }
        for (URL url : recoverRegistered) {
            register(url);
        }
    }
    // subscribe
    Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<>(getSubscribed());
    if (!recoverSubscribed.isEmpty()) {
        if (logger.isInfoEnabled()) {
            logger.info("Recover subscribe url " + recoverSubscribed.keySet());
        }
        for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
            URL url = entry.getKey();
            for (NotifyListener listener : entry.getValue()) {
                subscribe(url, listener);
            }
        }
    }
}

我把recover 方法分成类两部分,先是恢复register 注册的,在就是恢复那些订阅者。
先看下恢复注册的,就是获取缓存在registered的url们,然后不是空的话就遍历注册。再看下恢复订阅信息,跟恢复注册的差不多,获取缓存在subscribed的订阅信息,遍历订阅。 我们再看下另外一个notify 方法,它与上面说的那个notify 方法是有差距的,上面那个方法是通知备份url的。

protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    if (url == null) {
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
        throw new IllegalArgumentException("notify listener == null");
    }
    if ((CollectionUtils.isEmpty(urls))
            && !ANY_VALUE.equals(url.getServiceInterface())) {
        logger.warn("Ignore empty notify urls for subscribe url " + url);
        return;
    }
    if (logger.isInfoEnabled()) {
        logger.info("Notify urls for subscribe url " + url + ", url size: " + urls.size());
    }
    // keep every provider's category.
    Map<String, List<URL>> result = new HashMap<>();
    for (URL u : urls) {
        if (UrlUtils.isMatch(url, u)) {
            String category = u.getCategory(DEFAULT_CATEGORY);
            List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
            categoryList.add(u);
        }
    }
    if (result.size() == 0) {
        return;
    }
    Map<String, List<URL>> categoryNotified = notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
    for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
        String category = entry.getKey();
        List<URL> categoryList = entry.getValue();
        categoryNotified.put(category, categoryList);
        // 主动调用各分类节点的notify(),更新到本地
        listener.notify(categoryList);
        // We will update our cache file after each notification.
        // When our Registry has a subscribe failure due to network jitter, we can return at least the existing cache URL.
        if (localCacheEnabled) {
            // todo 保存到properties中
            saveProperties(url);
        }
    }
}

上面那一堆就是验证是否为空的,接着遍历urls,根据category 来分类,然后从notified 已经通知的缓存中获取分类url信息,获取的结果是个map,Map<String, List<URL>> key是分组 ,value就是在该组下面的url。接着就是遍历上面分组出来的那个result,塞到从缓存中获取的那个map中,接着调用saveProperties 方法。最后进行通知。
我们看下这个saveProperties 方法,这个方法其实就是进行缓存文件同步的:

private void saveProperties(URL url) {
    if (file == null) {
        return;
    }

    try {
        StringBuilder buf = new StringBuilder();
        // 根据url从 已经通知集合中获取 分类 urls
        Map<String, List<URL>> categoryNotified = notified.get(url);
        if (categoryNotified != null) {
            // 遍历拼接 url
            for (List<URL> us : categoryNotified.values()) {
                for (URL u : us) {
                    if (buf.length() > 0) {
                        // 使用空字符串隔开
                        buf.append(URL_SEPARATOR);
                    }

                    buf.append(u.toFullString());
                }
            }
        }
        // key 接口全类名  value urls.toFullString
        properties.setProperty(url.getServiceKey(), buf.toString());
        // 获取版本
        long version = lastCacheChanged.incrementAndGet();
        // 同步保存
        if (syncSaveFile) {
            doSaveProperties(version);
        } else { //异步保存
            registryCacheExecutor.execute(new SaveProperties(version));
        }
    } catch (Throwable t) {
        logger.warn(t.getMessage(), t);
    }
}

先是根据url从已经通知缓存中获取对应的分类url集合,然后遍历拼接url.toFullString(),中间用空字符串隔开,然后将拼接的字符串塞进properties中,获取版本信息,判断syncSaveFile变量同步保存还是异步保存,同步的话就直接调用doSaveProperties方法进行保存,异步的话使用registryCacheExecutor线程池,提交任务的方式。先看下这个异步 SaveProperties类。

private class SaveProperties implements Runnable {
    private long version;

    private SaveProperties(long version) {
        this.version = version;
    }

    @Override
    public void run() {
        doSaveProperties(version);
    }
}

这里也是调用doSaveProperties方法,我们看下这个方法:

public void doSaveProperties(long version) {
    // 判断版本
    if (version < lastCacheChanged.get()) {
        return;
    }
    // 判断file
    if (file == null) {
        return;
    }
    // Save
    try {
        //创建锁文件。
        File lockfile = new File(file.getAbsolutePath() + ".lock");
        // 如果不存在的话就创建
        if (!lockfile.exists()) {
            lockfile.createNewFile();
        }
        try (RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
             FileChannel channel = raf.getChannel()) {
            //获取锁
            FileLock lock = channel.tryLock();
            if (lock == null) {
                throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
            }
            // Save
            try {
                // 如果缓存文件不存在就
                if (!file.exists()) {
                    file.createNewFile();
                }
                try (FileOutputStream outputFile = new FileOutputStream(file)) {
                    properties.store(outputFile, "Dubbo Registry Cache");
                }
            } finally {
                // 释放锁
                lock.release();
            }
        }
    } catch (Throwable e) {
        savePropertiesRetryTimes.incrementAndGet();
        if (savePropertiesRetryTimes.get() >= MAX_RETRY_TIMES_SAVE_PROPERTIES) {
            logger.warn("Failed to save registry cache file after retrying " + MAX_RETRY_TIMES_SAVE_PROPERTIES + " times, cause: " + e.getMessage(), e);
            savePropertiesRetryTimes.set(0);
            return;
        }
        if (version < lastCacheChanged.get()) {
            savePropertiesRetryTimes.set(0);
            return;
        } else {
            //出现异常 就异步保存
            registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
        }
        logger.warn("Failed to save registry cache file, will retry, cause: " + e.getMessage(), e);
    }
}

doSaveProperties这个方法中,也是比较简单,先是获取锁文件,就是在缓存文件名后面加.lock这个文件,接着就是获取FileLock 文件锁,将properties写入到对应注册中心缓存文件中,最后调用lock.release()释放文件锁,如果出现异常,就是用异步保存。 接下来我们再来看下lookup 方法,该方法就是根据url查找对应url集合的:

@Override
public List<URL> lookup(URL url) {
    List<URL> result = new ArrayList<>();
    //从已经通知 缓存中获取某个url对应的分组url集合
    Map<String, List<URL>> notifiedUrls = getNotified().get(url);
    // protocol不是空的话
    if (CollectionUtils.isNotEmptyMap(notifiedUrls)) {
        for (List<URL> urls : notifiedUrls.values()) {
            for (URL u : urls) {
                if (!EMPTY_PROTOCOL.equals(u.getProtocol())) {
                    result.add(u);
                }
            }
        }
    } else {
        //使用listener订阅的方式获取 urls
        final AtomicReference<List<URL>> reference = new AtomicReference<>();
        NotifyListener listener = reference::set;
        subscribe(url, listener); // Subscribe logic guarantees the first notify to return
        List<URL> urls = reference.get();
        if (CollectionUtils.isNotEmpty(urls)) {
            for (URL u : urls) {
                if (!EMPTY_PROTOCOL.equals(u.getProtocol())) {
                    result.add(u);
                }
            }
        }
    }
    return result;
}

lookup方法中先从已经通知notified 缓存集合中获取分组url的map集合,如果分组url的map集合 不是空然后遍历该集合,如果protocol不是empty的就添加到结果集合中返回。如果这个分组url的map集合是空,就实现一个listener,然后订阅该url ,如果有变动就会通知该listener的notify方法,该方法中就把 urls设置到reference中,调用reference获取urls,遍历urls,procotol不是empty的就添加到结果集合中返回。

2.3 FailbackRegistry

FailbackRegistry也是个抽象类,继承AbstractRegistry 抽象类,它主要是实现了失败重试的功能,下面我们就来看下它的实现。 先来看下它的class 定义

public abstract class FailbackRegistry extends AbstractRegistry {...}

接下来看下它的成员变量:

// 注册失败
private final ConcurrentMap<URL, FailedRegisteredTask> failedRegistered = new ConcurrentHashMap<URL, FailedRegisteredTask>();
// 注销失败
private final ConcurrentMap<URL, FailedUnregisteredTask> failedUnregistered = new ConcurrentHashMap<URL, FailedUnregisteredTask>();
// 订阅失败
private final ConcurrentMap<Holder, FailedSubscribedTask> failedSubscribed = new ConcurrentHashMap<Holder, FailedSubscribedTask>();
// 取消订阅失败
private final ConcurrentMap<Holder, FailedUnsubscribedTask> failedUnsubscribed = new ConcurrentHashMap<Holder, FailedUnsubscribedTask>();

/**
 * The time in milliseconds the retryExecutor will wait
 */
// 重试时间间隔
private final int retryPeriod;

// Timer for failure retry, regular check if there is a request for failure, and if there is, an unlimited retry
// 重试线程池
private final HashedWheelTimer retryTimer;

挨个解释下是干嘛的:

  • retryTimer :调度线程池,他这边是1个线程,主要是用来定时重试的。
  • failedRegistered:用来存放缓存注册失败url
  • failedUnregistered:用来存放缓存下线失败的url
  • failedSubscribed:用来存放缓存取消订阅的,主要是url与对应的listener
  • failedUnsubscribed:用来存放取消订阅失败信息,主要是url与对应的listener
  • retryPeriod:重试的时间间隔,默认是5秒

接下来看下构造方法:

public FailbackRegistry(URL url) {
    super(url);
    // 重试的间隔时间 默认是5秒
    this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);

    // since the retry task will not be very much. 128 ticks is enough.
    retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
}

可以看出来先是调用父类的单参构造,这里就是调用AbstractRegistry抽象类的构造,接着获取retry.period 参数值,这个表示重试的时间间隔,缺省的话是5 * 1000ms也就是5秒,再往下就是生成调度任务的,执行时间+每隔5秒 后执行一次。这个是采用时间轮机制 进行调度,大家感兴趣 可以从网上找相关文章进行学习。
接下来看下register 注册方法的实现:

// 注册,这个要注册的url就是业务url
@Override
public void register(URL url) {
    if (!acceptable(url)) {
        logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
        return;
    }
    // 检查缓存
    super.register(url);
    // 移除失败的缓存
    removeFailedRegistered(url);
    removeFailedUnregistered(url);
    try {
        // Sending a registration request to the server side
        doRegister(url);  // todo 继续注册
    } catch (Exception e) {
        Throwable t = e;

        // 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)
                && !(url.getPort() == 0);
        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
        // 将url添加到failedRegistered 中
        addFailedRegistered(url);
    }
}

可以看到先是调用父类的register方法进行注册,接着就是从 注册失败集合与下线失败集合中移除掉这个url,调用抽象方法doRegister(url);进行注册,该方法是个抽象方法,需要子类具体实现,protected abstract void doRegister(URL url);,如果出现异常,先是是否需要检查,需要跳过失败重试,如果是就直接抛出异常,不是的话就是打印error日志,将需要注册的url添加到failedRegistered这个缓存失败集合中。
接下来看下unregister下线方法:

@Override
public void unregister(URL url) {
    super.unregister(url);
    removeFailedRegistered(url);
    removeFailedUnregistered(url);
    try {
        // Sending a cancellation request to the server side
        // todo
        doUnregister(url);
    } catch (Exception e) {
        Throwable t = e;

        // 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)
                && !(url.getPort() == 0);
        boolean skipFailback = t instanceof SkipFailbackWrapperException;
        if (check || skipFailback) {
            if (skipFailback) {
                t = t.getCause();
            }
            throw new IllegalStateException("Failed to unregister " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
        } else {
            logger.error("Failed to unregister " + url + ", waiting for retry, cause: " + t.getMessage(), t);
        }

        // Record a failed registration request to a failed list, retry regularly
        addFailedUnregistered(url);
    }
}

这个方法跟注册方法register 差不多,先是调用父类的unregister方法进行下线,接着就是从 注册失败集合与下线失败集合中移除掉这个url,调用doUnregister(url)方法进行下线,doUnregister也是个抽象方法,需要具体的子类来实现,protected abstract void doUnregister(URL url); 出现异常判断是否检查与跳过失败重试,最后将url添加到failedUnregistered下线失败的集合中

接着再来看下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
        // todo
        doSubscribe(url, listener);  // 继续订阅
    } catch (Exception e) {
        Throwable t = e;

        List<URL> urls = getCacheUrls(url);
        if (CollectionUtils.isNotEmpty(urls)) {
            notify(url, listener, urls);
            logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(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);
    }
}

先是调用父类的订阅方法subscribe,接着调用removeFailedSubscribed方法移除失败订阅的缓存。再往后调用抽象方法doSubscribe(url, listener)进行订阅,protected abstract void doSubscribe(URL url, NotifyListener listener);该方法需要其子类具体实现,如果出现异常,现获取缓存的url集合,如果urls不是空就进行通知,如果是空的话需要是否检查与跳过失败重试,最后调用addFailedSubscribed(url, listener)方法将订阅失败的信息添加到failedSubscribed这个缓存中。这里removeFailedSubscribed方法与addFailedSubscribed需要看下。

public void removeFailedSubscribed(URL url, NotifyListener listener) {
    Holder h = new Holder(url, listener);
    // 移除 失败的订阅
    FailedSubscribedTask f = failedSubscribed.remove(h);
    if (f != null) {
        f.cancel();
    }
    // 移除 失败的取消订阅
    removeFailedUnsubscribed(url, listener);
}

在移除失败订阅信息removeFailedSubscribed方法中,先是从failedSubscribed 存放失败订阅的map中移除对应的listener,然后又从failedUnsubscribed取消订阅失败的集合中移除对应的url的listener,最后是从通知失败的集合failedNotified中移除对应的listener。

protected void addFailedSubscribed(URL url, NotifyListener listener) {
    Holder h = new Holder(url, listener);
    FailedSubscribedTask oldOne = failedSubscribed.get(h);
    if (oldOne != null) {
        return;
    }
    FailedSubscribedTask newTask = new FailedSubscribedTask(url, this, listener);
    oldOne = failedSubscribed.putIfAbsent(h, newTask);
    if (oldOne == null) {
        // never has a retry task. then start a new task for retry.
        retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
    }
}

在添加订阅失败addFailedSubscribed方法中就是根据url获取对应的集合,如果没有对应的集合就创建,然后将listener添加到集合中。

接下来看下取消订阅方法unsubscribe实现

@Override
public void unsubscribe(URL url, NotifyListener listener) {
    // 交给父类 缓存
    super.unsubscribe(url, listener);
    // 移除 失败的订阅
    removeFailedSubscribed(url, listener);
    try {
        // Sending a canceling subscription request to the server side
        doUnsubscribe(url, listener);
    } catch (Exception e) {
        Throwable t = e;

        // 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 unsubscribe " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
        } else {
            logger.error("Failed to unsubscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
        }

        // Record a failed registration request to a failed list, retry regularly
        addFailedUnsubscribed(url, listener);
    }
}

也是先调用父类的unsubscribe方法,接着调用removeFailedSubscribed方法移除一些缓存信息,再往后就是调用抽象方法doUnsubscribe(url, listener)进行取消订阅,该方法也是需要子类实现,protected abstract void doUnsubscribe(URL url, NotifyListener listener);如果出现异常需要判断是否检查与跳过失败重试,如果需要就抛出异常,如果不需要只是打印error日志,接着就是往failedUnsubscribed 存放取消订阅失败集合中添加这个取消信息了。

接着在看下notify通知方法实现:

@Override
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    if (url == null) {
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
        throw new IllegalArgumentException("notify listener == null");
    }
    try {
        // todo
        doNotify(url, listener, urls);
    } catch (Exception t) {
        // Record a failed registration request to a failed list
        logger.error("Failed to notify addresses for subscribe " + url + ", cause: " + t.getMessage(), t);
    }
}

先是检查url与listener,接着调用doNotify(url, listener, urls);方法进行通知:

protected void doNotify(URL url, NotifyListener listener, List<URL> urls) {
        super.notify(url, listener, urls);
}

可以看到doNotify方法就是调用父类notify方法通知。如果出现异常,就将通知信息添加到failedNotified 这个存放通知失败的集合中。

接下来再来看下这个recover恢复方法:

@Override
protected void recover() throws Exception {
    // register
    // 获取已经注册过的
    Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
    if (!recoverRegistered.isEmpty()) {
        if (logger.isInfoEnabled()) {
            logger.info("Recover register url " + recoverRegistered);
        }
        for (URL url : recoverRegistered) {
            // remove fail registry or unRegistry task first.
            removeFailedRegistered(url);
            removeFailedUnregistered(url);
            // 添加到失败注册中
            addFailedRegistered(url);
        }
    }
    // subscribe
    // 获取已经订阅过的
    Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
    if (!recoverSubscribed.isEmpty()) {
        if (logger.isInfoEnabled()) {
            logger.info("Recover subscribe url " + recoverSubscribed.keySet());
        }
        for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
            URL url = entry.getKey();
            for (NotifyListener listener : entry.getValue()) {
                // First remove other tasks to ensure that addFailedSubscribed can succeed.
                removeFailedSubscribed(url, listener);
                // 添加到失败订阅中
                addFailedSubscribed(url, listener);
            }
        }
    }
}

可以看到有两部分,先是调用父类getRegistered方法获取需要恢复注册的url,然后循环添加到注册失败的集合failedRegistered中,这里这么做的原因是有失败重试的定时任务会进行重新注册。下面那部分是调用getSubscribed方法获取已经订阅的信息,然后遍历添加到失败的订阅的集合failedSubscribed 中,定时任务也会给他重新订阅。

最后我们再来看下 时间轮机制中失败重试的 方法,该方法是失败重试定时任务调用的,用来失败重试的。抽象类,

public abstract class AbstractRetryTask implements TimerTask {

    ...
    @Override
    public void run(Timeout timeout) throws Exception {
        if (timeout.isCancelled() || timeout.timer().isStop() || isCancel()) {
            // other thread cancel this timeout or stop the timer.
            return;
        }
        if (times > retryTimes) {
            // reach the most times of retry.
            logger.warn("Final failed to execute task " + taskName + ", url: " + url + ", retry " + retryTimes + " times.");
            return;
        }
        if (logger.isInfoEnabled()) {
            logger.info(taskName + " : " + url);
        }
        try {
            doRetry(url, registry, timeout);
        } catch (Throwable t) { // Ignore all the exceptions and wait for the next retry
            logger.warn("Failed to execute task " + taskName + ", url: " + url + ", waiting for again, cause:" + t.getMessage(), t);
            // reput this task when catch exception.
            reput(timeout, retryPeriod);
        }
    }
    // 具体子类实现
    protected abstract void doRetry(URL url, FailbackRegistry registry, Timeout timeout);
}
  • 注册失败的重试,遍历注册失败集合,然后调用 doRegister(url);进行重新注册,接着就是从failedRegistered移除该url。

    public final class FailedRegisteredTask extends AbstractRetryTask {
    
        private static final String NAME = "retry register";
    
        public FailedRegisteredTask(URL url, FailbackRegistry registry) {
            super(url, registry, NAME);
        }
    
        @Override
        protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
            registry.doRegister(url);
            registry.removeFailedRegisteredTask(url);
        }
    }
    
  • 下线失败的重试,也是遍历下线失败集合,然后调用doUnregister(url); 方法进行下线,从failedUnregistered移除该url。

    public final class FailedUnregisteredTask extends AbstractRetryTask {
    
        private static final String NAME = "retry unregister";
    
        public FailedUnregisteredTask(URL url, FailbackRegistry registry) {
            super(url, registry, NAME);
        }
    
        @Override
        protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
            registry.doUnregister(url);
            registry.removeFailedUnregisteredTask(url);
        }
    }
    
  • 订阅失败的重试,也是一样的,遍历调用doSubscribe 方法进行重新订阅,然后从集合中移除对应的listener。

    public final class FailedSubscribedTask extends AbstractRetryTask {
    
        private static final String NAME = "retry subscribe";
    
        private final NotifyListener listener;
    
        public FailedSubscribedTask(URL url, FailbackRegistry registry, NotifyListener listener) {
            super(url, registry, NAME);
            if (listener == null) {
                throw new IllegalArgumentException();
            }
            this.listener = listener;
        }
    
        @Override
        protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
            registry.doSubscribe(url, listener);
            registry.removeFailedSubscribedTask(url, listener);
        }
    }
    
  • 取消订阅的失败重试,也是遍历调用doUnsubscribe(url, listener)方法进行重新取消订阅,然后从集合中移除对应的listener。

    public final class FailedUnsubscribedTask extends AbstractRetryTask {
    
        private static final String NAME = "retry unsubscribe";
    
        private final NotifyListener listener;
    
        public FailedUnsubscribedTask(URL url, FailbackRegistry registry, NotifyListener listener) {
            super(url, registry, NAME);
            if (listener == null) {
                throw new IllegalArgumentException();
            }
            this.listener = listener;
        }
    
        @Override
        protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
            registry.unsubscribe(url, listener);
            registry.removeFailedUnsubscribedTask(url, listener);
        }
    }
    

2.4 总结

注册中心抽象实现就解析完成了,主要是解析了注册中心工厂的抽象实现AbstractRegistryFactory,注册中心抽象AbstractRegistry与FailbackRegistry,在AbstractRegistryFactory 中,主要是处理了对注册中心对象的缓存。AbstractRegistry中主要是处理了对注册,订阅,取消注册,取消订阅,已经通知的缓存,并且提供了注册中心本地缓存与恢复。FailbackRegistry 中主要是处理了对注册失败,取消注册失败,订阅失败,取消订阅失败,通知失败的缓存,并且进行重试工作。

3. 注册中心zookeeper实现

3.1 ZookeeperRegistryFactory

ZookeeperRegistryFactory是注册中心zookeeper工厂的实现,代码比较简单,我们直接来看下。

public class ZookeeperRegistryFactory extends AbstractRegistryFactory {

    private ZookeeperTransporter zookeeperTransporter;

    public ZookeeperRegistryFactory() {
        this.zookeeperTransporter = ZookeeperTransporter.getExtension();
    }

    @DisableInject
    // 由dubbo spi自动注入 zk客户端
    public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
        this.zookeeperTransporter = zookeeperTransporter;
    }

    @Override
    public Registry createRegistry(URL url) {
        return new ZookeeperRegistry(url, zookeeperTransporter);
    }

}

可以看到ZookeeperRegistryFactory继承AbstractRegistryFactory抽象类,需要实现createRegistry方法,在createRegistry方法中,创建了一个zookeeper注册中心对象,参数分别是注册中心url与zookeeperTransporter (这个是dubbo spi 自动注入的,zookeeperTransporter能够获取zk客户端对象,这个文章后面会有说到)

3.2 ZookeeperRegistry

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

3.2.1 class与成员变量

public class ZookeeperRegistry extends FailbackRegistry {...}

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

private final static String DEFAULT_ROOT = "dubbo";

// 根路径
private final String root;

private final Set<String> anyServices = new ConcurrentHashSet<>();
// 缓存的订阅listener
private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<>();
// zk客户端
private final ZookeeperClient zkClient;

3.2.2 构造

接下来看下构造方法:

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    if (url.isAnyHost()) {
        throw new IllegalStateException("registry address == null");
    }
    // group  默认dubbo 获取根路径
    String group = url.getGroup(DEFAULT_ROOT);
    if (!group.startsWith(PATH_SEPARATOR)) {
        group = PATH_SEPARATOR + group;
    }
    this.root = group;
    // 创建zk客户端,与zk进行连接 默认使用curator
    zkClient = zookeeperTransporter.connect(url);
    zkClient.addStateListener((state) -> {
        // 状态是 已连接的 时候
        if (state == StateListener.RECONNECTED) {
            logger.warn("Trying to fetch the latest urls, in case there're provider changes during connection loss.\n" +
                    " Since ephemeral ZNode will not get deleted for a connection lose, " +
                    "there's no need to re-register url of this instance.");
            ZookeeperRegistry.this.fetchLatestAddresses();
        } else if (state == StateListener.NEW_SESSION_CREATED) {
            logger.warn("Trying to re-register urls and re-subscribe listeners of this instance to registry...");
            try {
                // 恢复 注册 与订阅
                ZookeeperRegistry.this.recover();
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        } else if (state == StateListener.SESSION_LOST) {
            logger.warn("Url of this instance will be deleted from registry soon. " +
                    "Dubbo client will try to re-register once a new session is created.");
        } else if (state == StateListener.SUSPENDED) {

        } else if (state == StateListener.CONNECTED) {

        }
    });
}

在构造方法中,先是调用父类的构造,接着判断注册中心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实现关系:

image.png

再来看下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客户端具体实现我们会后面说下。

3.2.3 doRegister

接着我们看下ZookeeperRegistry的doRegister进行注册方法实现:

@Override
public void doRegister(URL url) {
    try {  // 创建一个临时节点
        zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
    } catch (Throwable e) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

我们可以看到使用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) + 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.hsf.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.hsf.dubbo.provider.IHelloProviderService%26dubbo%3D2.0.2%26generic%3Dfalse%26interface%3Dcom.xuzhaocai.dubbo.provider.IHelloProviderService%26methods%3DgetName%26pid%3D39753%26side%3Dprovider%26timestamp%3D1598241007805

在zk中大概就是下图这样一个结构(树结构):

image.png

3.2.4 doUnregister

接着我们再来看下doUnregister 下线方法实现:

@Override
public void doUnregister(URL url) {
    try {
        zkClient.delete(toUrlPath(url));
    } catch (Throwable e) {
        throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

这里就是调用zk客户端的delete方法删除url对应path的节点。

3.2.5 doSubscribe

接下来再来看下doSubscribe 进行订阅的方法(由于方法比较长,分为2部分): 第一部分是订阅所有

image.png

当url的service interface 是 * 的时候,表示要订阅所有,先是获取root,订阅所有,其实就是订阅根节点下面的所有子节点,接着从缓存中获取url对应的listener map ,这里为啥是map呢,其实一个url可以多个订阅者(也就可以有多个监听器),每个监听器又对应一个具体的子节点监听器。如果缓存中没有存在,就创建。如果没有获取到子节点监听器,也创建ChildListener对象,我们可以看看它里面的实现:

ChildListener zkListener = listeners.computeIfAbsent(listener, k -> (parentPath, currentChilds) -> {
    // 遍历child节点
    for (String child : currentChilds) {
        child = URL.decode(child);
        if (!anyServices.contains(child)) {
            anyServices.add(child);
            // 订阅
            subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
                    Constants.CHECK_KEY, String.valueOf(false)), k);
        }
    }
});

当订阅节点下面的子节点发生变化的时候,就会调用childChanged 方法,parentPath 表示父节点,也就是该节点下面的子节点发生了变化,currentChilds表示当前parentPath 节点下面所有的子节点集合。在childChanged方法中,对子节点集合进行遍历,然后如果这个子节点没有在anyServices 集合中,就添加进去(anyServices 集合 就是存的所有的接口全类名,也就是根节点下的所有子节点),接着就是订阅这个子节点。

doSubscribe 方法接着往下看,使用zk客户端创建root节点,对root节点添加监听器listener,获取到root节点(也就是根节点 比如/dubbo)子节点集合,遍历集合添加到anyServices 集合中,然后订阅这堆子节点。

接着再看看第二部分,订阅某个接口的时候。

 if(){
 ....
 } else {  // 处理<dubbo:reference/>的interface属性为普通值的情况
    CountDownLatch latch = new CountDownLatch(1);
    List<URL> urls = new ArrayList<>();

    // 遍历configurators、routers与providers三个路径
    for (String path : toCategoriesPath(url)) {
        ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
        ChildListener zkListener = listeners.computeIfAbsent(listener, k -> new RegistryChildListenerImpl(url, path, k, latch));
        if (zkListener instanceof RegistryChildListenerImpl) {
            ((RegistryChildListenerImpl) zkListener).setLatch(latch);
        }
        // 创建持久节点
        zkClient.create(path, false);
        // 为创建的节点添加子节点列表变更的watcher监听
        List<String> children = zkClient.addChildListener(path, zkListener);

        if (children != null) {
            // toUrlsWithEmpty
            urls.addAll(toUrlsWithEmpty(url, path, children));
        }
    }
    // 主动调用notify更新本地invoker
    notify(url, listener, urls);
    // tells the listener to run only after the sync notification of main thread finishes.
    latch.countDown();
}

该部分中,先是通过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 对象与上面那部分不一样(这里很重要)

public RegistryChildListenerImpl(URL consumerUrl, String path, NotifyListener listener, CountDownLatch latch) {
    this.latch = latch;
    notifier = new RegistryNotifier(ZookeeperRegistry.this.getDelay()) {
        @Override
        public void notify(Object rawAddresses) {
            long delayTime = getDelayTime();
            if (delayTime <= 0) {
                this.doNotify(rawAddresses);
            } else {
                long interval = delayTime - (System.currentTimeMillis() - lastExecuteTime);
                if (interval > 0) {
                    try {
                        Thread.sleep(interval);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                }
                lastExecuteTime = System.currentTimeMillis();
                this.doNotify(rawAddresses);
            }
        }

        @Override
        protected void doNotify(Object rawAddresses) {
            ZookeeperRegistry.this.notify(consumerUrl, listener, ZookeeperRegistry.this.toUrlsWithEmpty(consumerUrl, path, (List<String>) rawAddresses));
        }
    };
}

当订阅的节点子类发生变化的时候,就会调用监听器的childChanged 方法,在childChanged 方法 中调用了notify方法进行通知。

再往下看,就是创建这个path,在path上面添加监听器,能够获取现在path下面的子节点,然后把这些子节点经过过滤后添加到list中。

最后遍历完成后,调用notify通知(这里这样的做的原因是第一次订阅的时候,如果不通知,需要子节点变动的时候才能通知,才能获取到服务提供者列表),这里面有个toUrlsWithEmpty(url, path, children) 方法,该方法将 字符串子节点转成url集合,其实这个方法中有个比较重要的点就是过滤,需要与consumerUrl的某些条件匹配上。我们来看下:

protected List<URL> toUrlsWithEmpty(URL consumer, String path, Collection<String> providers) {
    List<URL> urls = new ArrayList<>(1);
    // 判断当前path是否以providers结尾
    boolean isProviderPath = path.endsWith(PROVIDERS_CATEGORY);
    if (isProviderPath) {
        // 处理providers节点情况
        if (CollectionUtils.isNotEmpty(providers)) {
            // 将providers节点的所有子节点变为url
            urls = toUrlsWithoutEmpty(consumer, providers);
        } else {
            // clear cache on empty notification: unsubscribe or provider offline
            evictURLCache(consumer);
        }
    } else {
        // 处理configurators与routers节点情况
        if (CollectionUtils.isNotEmpty(providers)) {
            // 将节点的所有子节点变为url
            urls = toConfiguratorsWithoutEmpty(consumer, providers);
        }
    }

    // 为没有子节点的节点创建一个 empty://...的URL
    if (urls.isEmpty()) {
        int i = path.lastIndexOf(PATH_SEPARATOR);
        String category = i < 0 ? path : path.substring(i + 1);
        URL empty = URLBuilder.from(consumer)
                .setProtocol(EMPTY_PROTOCOL)
                .addParameter(CATEGORY_KEY, category)
                .build();
        urls.add(empty);
    }

    return urls;
}

开始调用toUrlsWithoutEmpty(consumer, providers); 方法将字符串子节点转成url集合,我们来看下这个方法:

protected List<URL> toUrlsWithoutEmpty(URL consumer, Collection<String> providers) {
    // keep old urls
    Map<String, ServiceAddressURL> oldURLs = stringUrls.get(consumer);
    // create new urls
    Map<String, ServiceAddressURL> newURLs;
    URL copyOfConsumer = removeParamsFromConsumer(consumer);
    if (oldURLs == null) {
        newURLs = new HashMap<>();
        for (String rawProvider : providers) {
            rawProvider = stripOffVariableKeys(rawProvider);
            // todo
            ServiceAddressURL cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());
            if (cachedURL == null) {
                logger.warn("Invalid address, failed to parse into URL " + rawProvider);
                continue;
            }
            newURLs.put(rawProvider, cachedURL);
        }
    } else {
        newURLs = new HashMap<>((int) (oldURLs.size() / .75 + 1));
        // maybe only default , or "env" + default
        for (String rawProvider : providers) {
            rawProvider = stripOffVariableKeys(rawProvider);
            ServiceAddressURL cachedURL = oldURLs.remove(rawProvider);
            if (cachedURL == null) {
                cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());
                if (cachedURL == null) {
                    logger.warn("Invalid address, failed to parse into URL " + rawProvider);
                    continue;
                }
            }
            newURLs.put(rawProvider, cachedURL);
        }
    }

    evictURLCache(consumer);
    stringUrls.put(consumer, newURLs);

    return new ArrayList<>(newURLs.values());
}

接着就是调用createURL() -> isMatch(consumerURL, cachedURL)

这里就是遍历providers,然后需要两个条件才能添加到结果集合中,一个是判断 里面有没有:// 这个东西就是放在协议protocol后面的,这个有之后 ,将字符串就转成了url,然后调用UrlUtils.isMatch(consumer, url)方法进行匹配,匹配的上才能添加到结果集合中,这个isMatch 方法主要比对双方的 接口,category,enable,group,version,classifier 这些信息:

public static boolean isMatch(URL consumerUrl, URL providerUrl) {
    // 获取接口
    String consumerInterface = consumerUrl.getServiceInterface();
    // 获取接口
    String providerInterface = providerUrl.getServiceInterface();
    //FIXME accept providerUrl with '*' as interface name, after carefully thought about all possible scenarios I think it's ok to add this condition.
    // conusmerInterface 不是 *  或者consumerInterface 与providerInterface 不相等
    if (!(ANY_VALUE.equals(consumerInterface)
            || ANY_VALUE.equals(providerInterface)
            || StringUtils.isEquals(consumerInterface, providerInterface))) {
        return false;
    }
    // category 是符合要求的
    if (!isMatchCategory(providerUrl.getCategory(DEFAULT_CATEGORY),
            consumerUrl.getCategory(DEFAULT_CATEGORY))) {
        return false;
    }
    // provider enable 是false && consumer enable不是*
    if (!providerUrl.getParameter(ENABLED_KEY, true)
            && !ANY_VALUE.equals(consumerUrl.getParameter(ENABLED_KEY))) {
        return false;
    }

    // group
    String consumerGroup = consumerUrl.getGroup();
    // version
    String consumerVersion = consumerUrl.getVersion();
    // Classifier 默认是*
    String consumerClassifier = consumerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);

    String providerGroup = providerUrl.getGroup();
    String providerVersion = providerUrl.getVersion();
    String providerClassifier = providerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);
            // consumer category 是*  或者  category 相等 或者 consumer category 中包含着provider的category
    return (ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup))
            // consumer的version 是*  或者 两个version相等
            && (ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion))
            // consumerClassifier 是null 或者consumerClassifier 是* 或者相等
            && (consumerClassifier == null || ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier));
}

再回到toUrlsWithEmpty 方法中,转换成urls之后,如果是空的,就生成一个protocol 是empty的url塞进去返回,为什么要生成一个empty的url(这玩意就是代表着注册中心没有该consumer的实现了,本地缓存也就需要清空),原因是listener实现类notify方法中,看到就这一条empty url的时候就会清空对应的category下的url缓存。

到这我们的执行订阅doSubscribe 方法就讲解完成了

3.2.6 doUnsubscribe

接下来我们看下doUnsubscribe 执行取消订阅的方法的实现:

@Override
public void doUnsubscribe(URL url, NotifyListener listener) {
    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
    if (listeners != null) {
        ChildListener zkListener = listeners.remove(listener);
        if (zkListener != null) {
            if (ANY_VALUE.equals(url.getServiceInterface())) {
                String root = toRootPath();
                zkClient.removeChildListener(root, zkListener);
            } else {
                for (String path : toCategoriesPath(url)) {
                    zkClient.removeChildListener(path, zkListener);
                }
            }
        }

        if(listeners.isEmpty()){
            zkListeners.remove(url);
        }
    }
}

这个比较简单,就是从缓存zkListeners 中获取对应的listener map集合,然后再获取listener对应的ChildListener监听器,如果不是空的话,判断接口是不是*,如果是,就调用zk客户端的removeChildListener方法移除根节点下的对应listener。

如果不是,就循环移除分组path的对应listener。

3.3 总结

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

4. zk客户端实现

4.1 ZookeeperTransporter

ZookeeperTransporter其实就是使用dubbo spi获取zk客户端的接口

@SPI
public interface ZookeeperTransporter {

    String CURATOR_5 = "curator5";

    String CURATOR = "curator";

    ZookeeperClient connect(URL url);

    static ZookeeperTransporter getExtension() {
        ExtensionLoader<ZookeeperTransporter> extensionLoader = getExtensionLoader(ZookeeperTransporter.class);
        boolean isHighVersion = isHighVersionCurator();
        if (isHighVersion) {
            return extensionLoader.getExtension(CURATOR_5);
        }
        return extensionLoader.getExtension(CURATOR);
    }

    static boolean isHighVersionCurator() {
        try {
            Class.forName("org.apache.curator.framework.recipes.cache.CuratorCache");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

}

我们看下它的2个实现类CuratorZookeeperTransporter 与 Curator5ZookeeperTransporter

Curator5ZookeeperTransporter:

public class Curator5ZookeeperTransporter extends AbstractZookeeperTransporter {

    @Override
    public ZookeeperClient createZookeeperClient(URL url) {
        return new Curator5ZookeeperClient(url);
    }

我们可以看到connect方法实现就是创建Curator5ZookeeperTransporter 对象
CuratorZookeeperTransporter:

public class CuratorZookeeperTransporter extends AbstractZookeeperTransporter {
    @Override
    public ZookeeperClient createZookeeperClient(URL url) {
        return new CuratorZookeeperClient(url);
    }
}

CuratorZookeeperTransporter的connect实现就是创建CuratorZookeeperClient 对象。

在这个根据isHighVersionCurator()返回使用那种对象。

4.2 ZookeeperClient

ZookeeperClient接口是dubbo 对zk操作的抽象,这样子做的原因是,java版本的zk客户端有curator与zkClient种,为了能够自由切换,就需要抽象出来一系列操作,分别由不同的客户端来进行实现。这样能够保证系统的灵活性,扩展性,所谓灵活性就是可以根据用户喜好自由配置,扩展性就是以后出现另一种客户端,只需要实现ZookeeperClient接口就可以了。在V3.0版本中,只有curator实现了。

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();

}

上面就是dubbo对zk封装的一系列操作。我们来看下它的继承关系图

image.png
可以看到还是很明显的,接下来的任务就是一一解析了。

4.3 AbstractZookeeperClient

AbstractZookeeperClient是zk客户端的抽象实现。它做了一些基本的操作,具体的操作还是由子类来实现的。

public abstract class AbstractZookeeperClient<TargetChildListener> implements ZookeeperClient {...}

可以看到AbstractZookeeperClient实现ZookeeperClient 接口,其中TargetChildListener 是个泛型,就跟我们平常写的T一个意思,只不过它这里为了好看使用了TargetChildListener,从字面意思上这个泛型是个childListener监听器。

再来看下它的成员变量

private final URL url;  // 注册中心的url
// 状态listener 列表  CopyOnWriteArraySet:线程安全
private final Set<StateListener> stateListeners = new CopyOnWriteArraySet<StateListener>();
// 缓存着 path与listener的关系
private final ConcurrentMap<String, ConcurrentMap<ChildListener, TargetChildListener>> childListeners = new ConcurrentHashMap<String, ConcurrentMap<ChildListener, TargetChildListener>>();
private volatile boolean closed = false;// 关闭标识 volatile修饰

接下来看下构造,就是设置 注册中心url

public AbstractZookeeperClient(URL url) {
    this.url = url;
}

接下来就是create方法,创建某个节点

@Override
public void create(String path, boolean ephemeral) {
    if (!ephemeral) {
        if (persistentExistNodePath.contains(path)) {
            return;
        }
        if (checkExists(path)) {
            persistentExistNodePath.add(path);
            return;
        }
    }
    int i = path.lastIndexOf('/');
    if (i > 0) {
        create(path.substring(0, i), false);
    }
    if (ephemeral) {
        createEphemeral(path);
    } else {
        createPersistent(path);
        persistentExistNodePath.add(path);
    }
}

需要说明的是create 这个方法是一个递归,比如说你一个path是这个样子的(这里为了方便观察我回车换行了)

/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%3D47110%26side%3Dprovider%26timestamp%3D1598321369244

它是这样一个执行顺序的,先是判断path是否是个永久节点(ephemeral这个参数表示 是创建永久节点还是临时节点,true的话就是临时节点,false就是永久节点),如果是永久节点的话就需要调用checkExists(path)方法检查这个path是否存在,checkExists是个抽象方法,需要子类来具体实现protected abstract boolean checkExists(String path);,如果存在了就不需要创建了,接着就是从后面查找/这个分割符,如果是存在这个也就是i>0,就会调用create方法,这时候传入path的0位置到i位置那段,而且是个永久节点(注意这就进入递归了),上面那个path第一次 是

image.png 也就是全部,但是创建它需要创建下图这个

image.png 但是要想创建上图这个path就需要创建

image.png 就这样子最后要需要创建dubbo 这个节点(这个步骤里面存在就会返回)
创建的是这个样子的判断是临时节点还是永久节点,临时节点的话就调用createEphemeral(path)创建,永久节点的话就调用createPersistent(path)创建,需要注意的是这两个方法都是抽象方法,需要子类具体实现。

// 创建永久
protected abstract void createPersistent(String path);
// 创建临时
protected abstract void createEphemeral(String path);

接下来再来看下stateListener的两个操作:

// 添加StateListener
@Override
public void addStateListener(StateListener listener) {
    stateListeners.add(listener);
}
// 移除StateListener
@Override
public void removeStateListener(StateListener listener) {
    stateListeners.remove(listener);
}

可以看到很简单,就是操作stateListeners缓存集合进行添加跟移除。
接下来再来看下添加子监听器addChildListener 方法的实现

@Override
public List<String> addChildListener(String path, final ChildListener listener) {
    // 根据path获取 childListener 与 具体节点监听器的对应关系
    ConcurrentMap<ChildListener, TargetChildListener> listeners = childListeners.computeIfAbsent(path, k -> new ConcurrentHashMap<>());
    // 根据childListener 获取对应的  具体节点监听器
    TargetChildListener targetListener = listeners.computeIfAbsent(listener, k -> createTargetChildListener(path, k));
    // 进行添加
    return addTargetChildListener(path, targetListener);
}

在方法中,首先是根据path从childListeners缓存map中获取对应childListener 与 具体节点监听器的对应关系,如果不存在就创建,并且塞进去,接着又是根据childListener获取对应的具体节点监听器,如果不存在就调用createTargetChildListener(path, listener)方法进行创建然后塞到缓存中,最后是调用addTargetChildListener 方法进行添加。其中addTargetChildListener 与 createTargetChildListener 方法都是抽象方法,这里父类只是定义一套执行模版,定义了一套流程。 接着来看看removeChildListener移除方法:

@Override
public void removeChildListener(String path, ChildListener listener) {
    ConcurrentMap<ChildListener, TargetChildListener> listeners = childListeners.get(path);
    if (listeners != null) {
        TargetChildListener targetListener = listeners.remove(listener);
        if (targetListener != null) {
            removeTargetChildListener(path, targetListener);
        }
    }
}

可以看出就是移除对应path 在缓存中的对应ChildListener ,最后调用子类方法removeTargetChildListener 移除具体节点监听器。removeTargetChildListener这个方法也是抽象方法。

 protected abstract TargetChildListener createTargetChildListener(String path, ChildListener listener);
 protected abstract List<String> addTargetChildListener(String path, TargetChildListener listener);
 protected abstract void removeTargetChildListener(String path, TargetChildListener listener);

stateChanged 方法会通知stateListeners集合中所有StateListener状态发生了改变(就是调用循环内对应StateListener 对象的stateChanged方法)

// 状态改变的时候, 进行通知
protected void stateChanged(int state) {
    for (StateListener sessionListener : getSessionListeners()) {
        sessionListener.stateChanged(state);
    }
}

4.4 CuratorZookeeperClient

CuratorZookeeperClient是对curator操作zk的封装,也是dubbo操作zk客户端默认选择,curator里面其实封装了好多东西,我曾用它做过分布式id,其中提供的api也很好用,是现在操作zk的主流。

public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {...}

CuratorZookeeperClient 继承AbstractZookeeperClient抽象类,然后它的节点监视器类型是CuratorWatcher。

看下构造方法实现:

public CuratorZookeeperClient(URL url) {
    super(url);
    try {
        // 创建builder
        int timeout = url.getParameter(TIMEOUT_KEY, DEFAULT_CONNECTION_TIMEOUT_MS);
        int sessionExpireMs = url.getParameter(ZK_SESSION_EXPIRE_KEY, DEFAULT_SESSION_TIMEOUT_MS);
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                .connectString(url.getBackupAddress())
                .retryPolicy(new RetryNTimes(1, 1000))
                .connectionTimeoutMs(timeout)
                .sessionTimeoutMs(sessionExpireMs);
        String authority = url.getAuthority();
        if (authority != null && authority.length() > 0) {
            builder = builder.authorization("digest", authority.getBytes());
        }
        // 创建client
        client = builder.build();
        // 添加状态监听器
        client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));
        client.start();
        boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);
        if (!connected) {
            throw new IllegalStateException("zookeeper not connected");
        }
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

在构造中,先是创建一个builder,设置了连接信息(ip+port),超时时间是5s,重试策略。接着又是权限信息的设置,然后通过builder的build方法获取client(这里使用了建造者模式),接着就是添加状态监视器,当状态变动的时候就会调用stateChanged方法,在stateChanged方法实现中,其实就是做了curator状态与dubbo zk 状态的转变,然后调用CuratorZookeeperClient的stateChanged方法进行状态变动通知,最后调用start方法启动。 接下来看下添加节点与删除节点操作:

// 创建永久节点
@Override
public void createPersistent(String path) {
    try {
        client.create().forPath(path);
    } catch (NodeExistsException e) {
        logger.warn("ZNode " + path + " already exists.", e);
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}
// 创建临时节点
@Override
public void createEphemeral(String path) {
    try {
        client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
    } catch (NodeExistsException e) {
        logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration" +
                ", this duplication might be caused by a delete delay from the zk server, which means the old expired session" +
                " may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, " +
                "we can just try to delete and create again.", e);
        deletePath(path);
        createEphemeral(path);
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

@Override
protected void createPersistent(String path, String data) {
    byte[] dataBytes = data.getBytes(CHARSET);
    try {
        client.create().forPath(path, dataBytes);
    } catch (NodeExistsException e) {
        try {
            client.setData().forPath(path, dataBytes);
        } catch (Exception e1) {
            throw new IllegalStateException(e.getMessage(), e1);
        }
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

@Override
protected void createEphemeral(String path, String data) {
    byte[] dataBytes = data.getBytes(CHARSET);
    try {
        client.create().withMode(CreateMode.EPHEMERAL).forPath(path, dataBytes);
    } catch (NodeExistsException e) {
        logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration" +
                ", this duplication might be caused by a delete delay from the zk server, which means the old expired session" +
                " may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, " +
                "we can just try to delete and create again.", e);
        deletePath(path);
        createEphemeral(path, data);
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}
// 删除节点
@Override
protected void deletePath(String path) {
    try {
        client.delete().deletingChildrenIfNeeded().forPath(path);
    } catch (NoNodeException ignored) {
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

都是直接调用curator客户端的api操作。

接下来看下createTargetChildListener创建节点监听器方法实现:

@Override
public CuratorWatcher createTargetChildListener(String path, ChildListener listener) {
    return new CuratorWatcherImpl(listener);
}

可以看到创建了CuratorWatcherImpl对象返回,这个CuratorWatcherImpl其实是CuratorWatcher的实现。

// 实现了CuratorWatcher接口
static class CuratorWatcherImpl implements CuratorWatcher {

    private CuratorFramework client;
    private volatile ChildListener childListener;
    private String path;
    
    public CuratorWatcherImpl(CuratorFramework client, ChildListener listener, String path) {
        this.client = client;
        this.childListener = listener;
        this.path = path;
    }

    protected CuratorWatcherImpl() {
    }

    public void unwatch() {
        this.childListener = null;
    }

    @Override
    public void process(WatchedEvent event) throws Exception {
        // if client connect or disconnect to server, zookeeper will queue
        // watched event(Watcher.Event.EventType.None, .., path = null).
        if (event.getType() == Watcher.Event.EventType.None) {
            return;
        }
        // 获取当前监听器节点的子节点
        if (childListener != null) {
            childListener.childChanged(path, client.getChildren().usingWatcher(this).forPath(path));
        }
    }
}

当节点监听器监听的那个节点的子节点发生变化的时候,就会调用对应的process 方法,先是判断listener是否是null,获取监听的path,然后调用listener的childChanged方法进行变动通知。

再来看下addTargetChildListener添加节点监听器方法实现:

@Override
public List<String> addTargetChildListener(String path, CuratorWatcherImpl listener) {
    try {
        return client.getChildren().usingWatcher(listener).forPath(path);
    } catch (NoNodeException e) {
        return null;
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

removeTargetChildListener移除节点监听器方法:

@Override
public void removeTargetChildListener(String path, CuratorWatcherImpl listener) {
    listener.unwatch();
}

这里直接调用了节点监听器的unwatch方法,这个unwatch其实就是把ChildListener的链接制成null,这样变动的时候就不会调用ChildListener的childChanged方法进行通知了,而没有直接像zkClient那样remove

参考文章

Dubbo3.0源码注释github地址
深度剖析Apache Dubbo核心技术内幕
dubbo源码系列
dubbo源码分析专栏

おすすめ

転載: juejin.im/post/7120881237396291615