深度解析dubbo注册中心抽象实现


一、AbstractRegistryFactory

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

public abstract class AbstractRegistryFactory implements RegistryFactory {...}

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

// 日志
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRegistryFactory.class);

// 在创建 注册中心对象或者 销毁注册中心对象的时候会用到 锁
private static final ReentrantLock LOCK = new ReentrantLock();

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

接下来看下getRegistry方法实现:

 @Override
    public Registry getRegistry(URL url) {
        url = url.setPath(RegistryService.class.getName())
                // interface
                .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
                // export  refer
                .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
        String key = url.toServiceString();//zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService
        // Lock the registry access process to ensure a single instance of the registry
        LOCK.lock();
        try {

            // 从缓存中获取, 有就返回, 没有就创建
            Registry registry = REGISTRIES.get(key);
            if (registry != null) {
                return registry;
            }

            // 创建方法由子类实现  模板方法设计模式
            registry = createRegistry(url);
            if (registry == null) {
                throw new IllegalStateException("Can not create registry " + url);
            }
            REGISTRIES.put(key, registry);//将 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方法来销毁,最后释放锁。

二、AbstractRegistry

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

public abstract class AbstractRegistry implements Registry {...}

接下来再来看下它的构造

 public AbstractRegistry(URL url) {
  	setUrl(url);// 设置 注册 url
    // Start file save timer  是否同步保存到文件中
    syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
    /// 生成文件名
    String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
    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 store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
            }
        }
    }
    this.file = file;
    loadProperties();
    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

我们可以看下具体内容是什么
在这里插入图片描述
接着就是判断这个文件是否存在了,创建file,将file赋值给成员变量的file。
在往后调用loadProperties();加载这个文件里面的键值对了。我们看下这个方法
在这里插入图片描述
可以看到就是把刚才那个缓存文件解析成properties对象,这个很简单。
接着构造方法往后看,notify(url.getBackupUrls());这个就是从注册中心url中找到backup属性值,这个backup属性值就是需要恢复的一些服务提供者url,再调用notify进行通知,将那些恢复的url通知给订阅方,我们看下notify方法
在这里插入图片描述
到这咱们的构造方法就解析完成了
接下来看下它的成员有哪些:
在这里插入图片描述
properties这个成员就是咱们loadProperties方法那个存储注册中心缓存的。
registryCacheExecutor这个成员是进行注册中心缓存的线程。
syncSaveFile 是否同步保存到文件。
lastCacheChanged 记录最后一次缓存的版本
registered 存放已经注册的url
subscribed 存放订阅的信息,key是订阅的url,然后value就是那些订阅者,一旦key这个url有变化,就通知value这堆订阅者
notified存放已经通知的信息
接下来看下register注册方法:
在这里插入图片描述
可以看到就是检查注册url,然后添加到registered 这个set里面缓存起来。
接着看下unregister取消注册方法:
在这里插入图片描述
就是检查注册url是否是空,然后从registered 这个set移除掉。
接着看下subscribe 订阅方法的实现:
在这里插入图片描述
可以看到一开始检查订阅url与listener,接着就是根据订阅url获取subscribed 中的监听集合,如果没有就新建塞进去,最后将listener添加到监听集合中。
接着看下unsubscribe取消订阅的实现:
在这里插入图片描述
就是从监听集合移除这个listener
接着我们再来看下其他方法:
先来看下recover 恢复方法(该方法在子类重连接的时候会被用到)
在这里插入图片描述
我把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 ((urls == null || urls.isEmpty())
                && !Constants.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 + ", urls: " + urls);
        }
        Map<String, List<URL>> result = new HashMap<String, List<URL>>();
        for (URL u : urls) {
            if (UrlUtils.isMatch(url, u)) {
                String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
                List<URL> categoryList = result.get(category);
                if (categoryList == null) {
                    categoryList = new ArrayList<URL>();
                    result.put(category, categoryList);
                }
                categoryList.add(u);
            }
        }
        if (result.size() == 0) {
            return;
        }
        Map<String, List<URL>> categoryNotified = notified.get(url);
        if (categoryNotified == null) {
            notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
            categoryNotified = notified.get(url);
        }
        for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
            String category = entry.getKey();
            List<URL> categoryList = entry.getValue();
            categoryNotified.put(category, categoryList);

            // 保存到properties中
            saveProperties(url);
            listener.notify(categoryList);
        }
    }

上面那一堆就是验证是否为空的,接着遍历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类
在这里插入图片描述
这里也是调用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();
            }
            RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
            try {
                FileChannel channel = raf.getChannel();
                try {
                    //获取锁
                    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();
                        }
                        FileOutputStream outputFile = new FileOutputStream(file);
                        try {
                            properties.store(outputFile, "Dubbo Registry Cache");
                        } finally {
                            outputFile.close();
                        }
                    } finally {
                        // 释放锁
                        lock.release();
                    }
                } finally {
                    channel.close();
                }
            } finally {
                raf.close();
            }
        } catch (Throwable e) {
            if (version < lastCacheChanged.get()) {
                return;
            } else {//出现异常 就异步保存
                registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
            }
            logger.warn("Failed to save registry store file, 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对应的分组url集合
        Map<String, List<URL>> notifiedUrls = getNotified().get(url);
        if (notifiedUrls != null && notifiedUrls.size() > 0) {
            for (List<URL> urls : notifiedUrls.values()) {
                for (URL u : urls) {
                    // protocol不是空的话
                    if (!Constants.EMPTY_PROTOCOL.equals(u.getProtocol())) {
                        result.add(u);
                    }
                }
            }
        } else {
            //使用listener订阅的方式获取 urls
            final AtomicReference<List<URL>> reference = new AtomicReference<List<URL>>();
            NotifyListener listener = new NotifyListener() {
                @Override
                public void notify(List<URL> urls) {
                    reference.set(urls);
                }
            };
            subscribe(url, listener); // Subscribe logic guarantees the first notify to return
            List<URL> urls = reference.get();
            if (urls != null && !urls.isEmpty()) {
                for (URL u : urls) {
                    if (!Constants.EMPTY_PROTOCOL.equals(u.getProtocol())) {
                        result.add(u);
                    }
                }
            }
        }
       

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

三、FailbackRegistry

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

public abstract class FailbackRegistry extends AbstractRegistry {...}

接下来看下它的成员变量:
在这里插入图片描述
挨个解释下是干嘛的:
retryExecutor :调度线程池,他这边是1个线程,主要是用来定时重试的。
retryFuture:定时任务
failedRegistered:用来存放缓存注册失败url
failedUnregistered:用来存放缓存下线失败的url
failedSubscribed:用来存放缓存取消订阅的,主要是url与对应的listener
failedUnsubscribed:用来存放取消订阅失败信息,主要是url与对应的listener
failedNotified:用来缓存通知失败的
retryPeriod:重试的时间间隔,默认是5秒
接下来看下构造方法:

 public FailbackRegistry(URL url) {
        super(url);
        //retry.period   默认是5000 ms  5s
        this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
        this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                // Check and connect to the registry
                try {
                    retry();
                } catch (Throwable t) { // Defensive fault tolerance
                    logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
                }
            }
        }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
    }

可以看出来先是调用父类的单参构造,这里就是调用AbstractRegistry抽象类的构造,接着获取retry.period 参数值,这个表示重试的时间间隔,缺省的话是5*1000ms也就是5秒,再往下就是生成调度任务的,执行时间+每隔5秒 后执行一次,任务里调用retry()方法进行重试,关于retry 方法我们后面会解析。
接下来看下register 注册方法的实现:
在这里插入图片描述
可以看到先是调用父类的register方法进行注册,接着就是从 注册失败集合与下线失败集合中移除掉这个url,调用抽象方法doRegister(url);进行注册,该方法是个抽象方法,需要子类具体实现,protected abstract void doRegister(URL url);,如果出现异常,先是是否需要检查,需要跳过失败重试,如果是就直接抛出异常,不是的话就是打印error日志,将需要注册的url添加到failedRegistered这个缓存失败集合中。
接下来看下unregister下线方法:
在这里插入图片描述
这个方法跟注册方法register 差不多,先是调用父类的unregister方法进行下线,接着就是从 注册失败集合与下线失败集合中移除掉这个url,调用doUnregister(url)方法进行下线,doUnregister也是个抽象方法,需要具体的子类来实现,protected abstract void doUnregister(URL url);
出现异常判断是否检查与跳过失败重试,最后将url添加到failedUnregistered下线失败的集合中
接着再来看下subscribe订阅方法的实现:
在这里插入图片描述
先是调用父类的订阅方法subscribe,接着调用removeFailedSubscribed方法移除失败订阅的缓存。再往后调用抽象方法doSubscribe(url, listener)进行订阅,protected abstract void doSubscribe(URL url, NotifyListener listener);该方法需要其子类具体实现,如果出现异常,现获取缓存的url集合,如果urls不是空就进行通知,如果是空的话需要是否检查与跳过失败重试,最后调用addFailedSubscribed(url, listener)方法将订阅失败的信息添加到failedSubscribed这个缓存中。这里removeFailedSubscribed方法与addFailedSubscribed需要看下。
在这里插入图片描述
在移除失败订阅信息removeFailedSubscribed方法中,先是从failedSubscribed 存放失败订阅的map中移除对应的listener,然后又从failedUnsubscribed取消订阅失败的集合中移除对应的url的listener,最后是从通知失败的集合failedNotified中移除对应的listener。
在这里插入图片描述
在添加订阅失败addFailedSubscribed方法中就是根据url获取对应的集合,如果没有对应的集合就创建,然后将listener添加到集合中。
接下来看下取消订阅方法unsubscribe实现
在这里插入图片描述
也是先调用父类的unsubscribe方法,接着调用removeFailedSubscribed方法移除一些缓存信息,再往后就是调用抽象方法doUnsubscribe(url, listener)进行取消订阅,该方法也是需要子类实现,protected abstract void doUnsubscribe(URL url, NotifyListener listener);如果出现异常需要判断是否检查与跳过失败重试,如果需要就抛出异常,如果不需要只是打印error日志,接着就是往failedUnsubscribed 存放取消订阅失败集合中添加这个取消信息了。
接着在看下notify通知方法实现:
在这里插入图片描述
先是检查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恢复方法:
在这里插入图片描述
可以看到有两部分,先是调用父类getRegistered方法获取需要恢复注册的url,然后循环添加到注册失败的集合failedRegistered中,这里这么做的原因是有失败重试的定时任务会进行重新注册。下面那部分是调用getSubscribed方法获取已经订阅的信息,然后遍历添加到失败的订阅的集合failedSubscribed 中,定时任务也会给他重新订阅。
最后我们再来看下retry 方法,该方法是失败重试定时任务调用的,用来失败重试的(由于方法很长,我们一部分一部分的解释)
在这里插入图片描述
该部分是注册失败的重试,遍历注册失败集合,然后调用 doRegister(url);进行重新注册,接着就是从failedRegistered移除该url。
在这里插入图片描述
该部分就是下线失败的重试,也是遍历下线失败集合,然后调用doUnregister(url); 方法进行下线,从failedUnregistered移除该url。
在这里插入图片描述
该部分是订阅失败的重试,也是一样的,遍历调用doSubscribe 方法进行重新订阅,然后从集合中移除对应的listener。
在这里插入图片描述
该部分是取消订阅的失败重试,也是遍历调用doUnsubscribe(url, listener)方法进行重新取消订阅,然后从集合中移除对应的listener。
在这里插入图片描述
该部分是通知失败的重试,也是遍历通知失败的集合,调用notify(urls); 方法进行重新通知,从集合中移除对应的listener。

四、总结

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

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