From ed48f9a816c4a7ccef9e1535df55212846ab1ee2 Mon Sep 17 00:00:00 2001
From: AmyliaY <471816751@qq.com>
Date: Sun, 19 Apr 2020 15:36:27 +0800
Subject: [PATCH] =?UTF-8?q?Dubbo=E6=B3=A8=E5=86=8C=E4=B8=AD=E5=BF=83?=
=?UTF-8?q?=E6=A8=A1=E5=9D=97=20=E6=BA=90=E7=A0=81=E8=A7=A3=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/Dubbo/registry/Dubbo注册中心.md | 2172 ++++++++++++++++-
images/Dubbo/Dubbo原理图.png | Bin 0 -> 71278 bytes
images/Dubbo/RegistryFactory组件类图.png | Bin 0 -> 49134 bytes
images/Dubbo/Registry组件类图.png | Bin 0 -> 66450 bytes
.../Dubbo/dubbo-registry模块结构图.png | Bin 0 -> 21661 bytes
5 files changed, 2171 insertions(+), 1 deletion(-)
create mode 100644 images/Dubbo/Dubbo原理图.png
create mode 100644 images/Dubbo/RegistryFactory组件类图.png
create mode 100644 images/Dubbo/Registry组件类图.png
create mode 100644 images/Dubbo/dubbo-registry模块结构图.png
diff --git a/docs/Dubbo/registry/Dubbo注册中心.md b/docs/Dubbo/registry/Dubbo注册中心.md
index fcb5dbe..2e94ce0 100644
--- a/docs/Dubbo/registry/Dubbo注册中心.md
+++ b/docs/Dubbo/registry/Dubbo注册中心.md
@@ -1 +1,2171 @@
-努力编写中...
\ No newline at end of file
+## 注册中心在Dubbo中的作用
+服务治理框架可以大致分为 服务通信 和 服务管理 两部分,服务管理可以分为服务注册、服务订阅以及服务发现,服务提供者Provider 会往注册中心注册服务,而消费者Consumer 会从注册中心中订阅自己关注的服务,并在关注的服务发生变更时 得到注册中心的通知。Provider、Consumer以及Registry之间的依赖关系 如下图所示。
+
+
+
+## dubbo-registry 模块 结构分析
+dubbo的注册中心有多种实现方案,如:zookeeper、redis、multicast等,本章先看一下 dubbo-registry 模块的核心部分 dubbo-registry-api,具体实现部分放到下章来讲。dubbo-registry模块 的结构如下图所示。
+
+
+
+### Registry 核心组件类图
+典型的 接口 -> 抽象类 -> 实现类 的结构设计,如下图所示。
+
+
+
+既然有Registry组件,那么按照很多框架的套路,肯定也有一个用于获取 Registry实例的RegistryFactory,其中用到了工厂方法模式,不同的工厂类用于获取不同类型的实例。其类图结构如下。
+
+
+
+## 源码详解
+根据上面的类图,我们开始从上往下 详解dubbo中对于注册中心的设计以及实现。
+### RegistryService 接口
+RegistryService 是注册中心模块的服务接口,定义了注册、取消注册、订阅、取消订阅以及查询符合条件的已注册数据 等方法。这里统一说明一下URL,dubbo是以总线模式来时刻传递和保存配置信息的,配置信息都被放在URL上进行传递,随时可以取得相关配置信息,而这里提到了URL有别的作用,就是作为类似于节点的作用,首先服务提供者(Provider)启动时需要提供服务,就会向注册中心写下自己的URL地址。然后消费者启动时需要去订阅该服务,则会订阅Provider注册的地址,并且消费者也会写下自己的URL。
+```java
+/**
+ * RegistryService. (SPI, Prototype, ThreadSafe)
+ *
+ * 注册中心服务接口
+ */
+public interface RegistryService {
+
+ /**
+ * 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则 等数据。
+ *
+ * 注册需处理契约:
+ * 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。
+ * 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。
+ * 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。
+ * 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。
+ * 5. 允许URI相同但参数不同的URL并存,不能覆盖。
+ *
+ * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
+ */
+ void register(URL url);
+
+ /**
+ * 取消注册.
+ *
+ * 取消注册需处理契约:
+ * 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。
+ * 2. 按全URL匹配取消注册。
+ *
+ * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
+ */
+ void unregister(URL url);
+
+ /**
+ * 订阅符合条件的已注册数据,当有注册数据变更时自动推送.
+ *
+ * 订阅需处理契约:
+ * 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。
+ * 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。
+ * 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0
+ * 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*
+ * 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。
+ * 6. 允许URI相同但参数不同的URL并存,不能覆盖。
+ * 7. 必须阻塞订阅过程,等第一次通知完后再返回。
+ *
+ * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
+ * @param listener 变更事件监听器,不允许为空
+ */
+ void subscribe(URL url, NotifyListener listener);
+
+ /**
+ * 取消订阅.
+ *
+ * 取消订阅需处理契约:
+ * 1. 如果没有订阅,直接忽略。
+ * 2. 按全URL匹配取消订阅。
+ *
+ * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
+ * @param listener 变更事件监听器,不允许为空
+ */
+ void unsubscribe(URL url, NotifyListener listener);
+
+ /**
+ * 查询符合条件的已注册数据,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
+ *
+ * @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
+ * @return 已注册信息列表,可能为空,含义同{@link com.alibaba.dubbo.registry.NotifyListener#notify(List)}的参数。
+ * @see com.alibaba.dubbo.registry.NotifyListener#notify(List)
+ */
+ List lookup(URL url);
+}
+```
+
+### Registry 接口
+注册中心接口,把节点Node 以及注册中心服务RegistryService 的方法整合在了这个接口里面。该接口并没有自己的方法,就是继承了Node和RegistryService接口。这里的Node是节点的接口,里面协定了关于节点的一些操作方法,源码如下。
+```java
+/**
+ * 注册中心接口
+ */
+public interface Registry extends Node, RegistryService {
+}
+
+public interface Node {
+ //获得节点地址
+ URL getUrl();
+ //判断节点是否可用
+ boolean isAvailable();
+ //销毁节点
+ void destroy();
+}
+```
+
+### AbstractRegistry 抽象类
+实现了Registry接口的抽象类。为了减轻注册中心的压力,该抽象类把本地URL缓存到了property文件中,并且实现了注册中心的注册、订阅等方法。
+```java
+/**
+ * 实现了Registry接口的抽象类,实现了如下方法:
+ *
+ * 1、通用的注册、订阅、查询、通知等方法
+ * 2、读取和持久化注册数据到文件,以 properties 格式存储
+ */
+public abstract class AbstractRegistry implements Registry {
+
+ // URL地址分隔符,用于文件缓存中,服务提供者URL分隔
+ private static final char URL_SEPARATOR = ' ';
+ // URL地址分隔正则表达式,用于解析文件缓存中服务提供者URL列表
+ private static final String URL_SPLIT = "\\s+";
+
+ // Log output
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+ /**
+ * 本地磁盘缓存。
+ * 1. 其中特殊的 key 值 .registies 记录注册中心列表 TODO 8019 芋艿,特殊的 key 是
+ * 2. 其它均为 {@link #notified} 服务提供者列表
+ */
+ private final Properties properties = new Properties();
+ /**
+ * 注册中心缓存写入执行器。
+ * 线程数=1
+ */
+ // File cache timing writing
+ private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
+ /**
+ * 是否同步保存文件
+ */
+ private final boolean syncSaveFile;
+ /**
+ * 数据版本号
+ */
+ private final AtomicLong lastCacheChanged = new AtomicLong();
+ /**
+ * 已注册 URL 集合。
+ * 注册的 URL 可以是服务提供者的,也可以是服务消费者的
+ */
+ private final Set registered = new ConcurrentHashSet();
+ /**
+ * 订阅 URL 的监听器集合
+ * key:订阅者的 URL ,例如消费者的 URL
+ */
+ private final ConcurrentMap> subscribed = new ConcurrentHashMap>();
+ /**
+ * 被通知的 URL 集合
+ * key1:消费者的 URL ,例如消费者的 URL ,和 {@link #subscribed} 的键一致
+ * key2:分类,例如:providers、consumers、routes、configurators。【实际无 consumers ,因为消费者不会去订阅另外的消费者的列表】
+ * 在 {@link Constants} 中,以 "_CATEGORY" 结尾
+ */
+ private final ConcurrentMap>> notified = new ConcurrentHashMap>>();
+ /**
+ * 注册中心 URL
+ */
+ private URL registryUrl;
+ /**
+ * 本地磁盘缓存文件,缓存注册中心的数据
+ */
+ private File file;
+ /**
+ * 是否销毁
+ */
+ private AtomicBoolean destroyed = new AtomicBoolean(false);
+
+ public AbstractRegistry(URL url) {
+ setUrl(url);
+ // Start file save timer
+ syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
+ // 获得 `file`
+ 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();
+ // 通知监听器,URL 变化结果
+ notify(url.getBackupUrls()); // 【TODO 8020】为什么构造方法,要通知,连监听器都没注册
+ }
+
+ protected static List filterEmpty(URL url, List urls) {
+ if (urls == null || urls.isEmpty()) {
+ List result = new ArrayList(1);
+ result.add(url.setProtocol(Constants.EMPTY_PROTOCOL));
+ return result;
+ }
+ return urls;
+ }
+
+ @Override
+ public URL getUrl() {
+ return registryUrl;
+ }
+
+ protected void setUrl(URL url) {
+ if (url == null) {
+ throw new IllegalArgumentException("registry url == null");
+ }
+ this.registryUrl = url;
+ }
+
+ public Set getRegistered() {
+ return registered;
+ }
+
+ public Map> getSubscribed() {
+ return subscribed;
+ }
+
+ public Map>> getNotified() {
+ return notified;
+ }
+
+ public File getCacheFile() {
+ return file;
+ }
+
+ public Properties getCacheProperties() {
+ return properties;
+ }
+
+ public AtomicLong getLastCacheChanged() {
+ return lastCacheChanged;
+ }
+
+ /**
+ * 保存内存缓存到本地磁盘缓存文件,即 {@link #properties} => {@link #file}
+ *
+ * @param version 数据版本号
+ */
+ public void doSaveProperties(long version) {
+ if (version < lastCacheChanged.get()) {
+ return;
+ }
+ if (file == null) {
+ return;
+ }
+ // Save
+ try {
+ // 创建 .lock 文件
+ 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();
+ }
+ // 释放文件 Channel
+ } 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);
+ }
+ }
+
+ /**
+ * 加载本地磁盘缓存文件到内存缓存,即 {@link #file} => {@link #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("Load registry store file " + file + ", data: " + properties);
+ }
+ } catch (Throwable e) {
+ logger.warn("Failed to load registry store file " + file, e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ logger.warn(e.getMessage(), e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 从 `properties` 中获得缓存的 URL 集合
+ *
+ * @param url URL
+ * @return URL 集合
+ */
+ public List getCacheUrls(URL url) {
+ for (Map.Entry