Dubbo3 Source Code Article 10 - Registration Center (Zookeeper and CuratorZookeeperClient)

Welcome everyone to pay attention to  github.com/hsfxuebao  , I hope it will be helpful to you. If you think it is possible, please click Star.

1. Registry API

1.1 Preview of the registry module

In the v3 version, the registry module exists in the form of a sub-project. Let's first take a look at the directory of the dubbo-registry sub-project:image.png

The dubbo-registry-api module mainly defines the interface of the registry and the registry Factory, and then the RegistryDirectory and RegistryProtocol classes and some surrounding support.

1.2 Introduction to Core Interfaces

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

}

We can see that the RegistryService registry service interface is mainly 抽象了注册,下线,订阅,取消订阅方法, and then there is a lookup method, this method will obtain all registered urls according to the provided url

1.2.2 RegistryFactory

Registry factory interface, which can dynamically find its subclasses based on the dubbo spi extension technology. The default implementation is DubboRegistryFactory.

@SPI("dubbo")
public interface RegistryFactory {

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

}

There is an abstract method to obtain the registry center. This method can adapt to the implementation class of the RegistryFactory according to the value of the protocol property of the url.

1.2.3 Registry

This Registry is the interface class of the registry, we can look at its inheritance relationship

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

Inherit Node and implement RegistryService. This RegistryService, as we mentioned in the section above, defines functions such as subscription, unsubscription, registration, and offline, and this Node actually means a node. We can see from its definition that it is relatively simple. Get the url, whether it is available, destroy these three functions.

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源码分析专栏

Guess you like

Origin juejin.im/post/7120881237396291615