精盡 Dubbo 源碼分析 —— 註冊中心抽象 API

1. 概述

本文分享 dubbo-registry-api 模塊,註冊中心模塊:基於註冊中心下發地址的集羣方式,以及對各種註冊中心的抽象。

在這裏插入圖片描述

2. 抽象 API

2.1 RegistryFactory

註冊中心工廠接口,代碼如下:

/**
 * 註冊中心工廠
 */
@SPI("dubbo")
public interface RegistryFactory {

    /**
     * 連接註冊中心.
     * <p>
     * 連接註冊中心需處理契約:<br>
     * 1. 當設置check=false時表示不檢查連接,否則在連接不上時拋出異常。<br>
     * 2. 支持URL上的username:password權限認證。<br>
     * 3. 支持backup=10.20.153.10備選註冊中心集羣地址。<br>
     * 4. 支持file=registry.cache本地磁盤文件緩存。<br>
     * 5. 支持timeout=1000請求超時設置。<br>
     * 6. 支持session=60000會話超時或過期設置。<br>
     *
     * @param url 註冊中心地址,不允許爲空
     * @return 註冊中心引用,總不返回空
     */
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);

}

2.2 AbstractRegistryFactory

實現 RegistryFactory 接口,RegistryFactory 抽象類,實現了 Registry 的容器管理。

2.2.1 屬性

    // The lock for the acquisition process of the registry
    private static final ReentrantLock LOCK = new ReentrantLock();

    /**
     * Registry 集合
     *
     * key:{@link URL#toServiceString()}
     */
    // Registry Collection Map<RegistryAddress, Registry>
    private static final Map<String, Registry> REGISTRIES = new ConcurrentHashMap<String, Registry>();

2.2.2 createRegistry

創建 Registry 對象。代碼如下:

   /**
     * 創建 Registry 對象
     *
     * @param url 註冊中心地址
     * @return Registry 對象
     */
    protected abstract Registry createRegistry(URL url);

子類實現該方法,創建其對應的 Registry 實現類。例如,ZookeeperRegistryFactory 的該方法,創建 ZookeeperRegistry 對象。

2.2.3 getRegistry

獲得註冊中心 Registry 對象。優先從緩存中獲取,否則進行創建。

/**
     * 獲得註冊中心 Registry 對象
     *
     * @param url 註冊中心地址,不允許爲空
     * @return Registry 對象
     */
    @Override
    public Registry getRegistry(URL url) {
        // 修改 URL
        url = url.setPath(RegistryService.class.getName()) // + `path`
                .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName()) // + `parameters.interface`
                .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY); // - `export`
        // 計算 key
        String key = url.toServiceString();
        // 獲得鎖
        // Lock the registry access process to ensure a single instance of the registry
        LOCK.lock();
        try {
            // 從緩存中獲得 Registry 對象
            Registry registry = REGISTRIES.get(key);
            if (registry != null) {
                return registry;
            }
            // 緩存不存在,進行創建 Registry 對象
            registry = createRegistry(url);
            if (registry == null) {
                throw new IllegalStateException("Can not create registry " + url);
            }
            // 添加到緩存
            REGISTRIES.put(key, registry);
            return registry;
        } finally {
            // 釋放鎖
            // Release the lock
            LOCK.unlock();
        }
    }

2.2.4 destroyAll

銷燬所有 Registry 對象。

/**
     * 銷燬所有 Registry
     *
     * Close all created registries
     */
    public static void destroyAll() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close all registries " + getRegistries());
        }
        // 獲得鎖
        // Lock up the registry shutdown process
        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();
        }
    }

2.3 RegistryService

註冊中心服務接口,定義了註冊、訂閱、查詢三種操作方法

public interface RegistryService {

    /**
     * 註冊數據,比如:提供者地址,消費者地址,路由規則,覆蓋規則,等數據。
     * <p>
     * 註冊需處理契約:<br>
     * 1. 當URL設置了check=false時,註冊失敗後不報錯,在後臺定時重試,否則拋出異常。<br>
     * 2. 當URL設置了dynamic=false參數,則需持久存儲,否則,當註冊者出現斷電等情況異常退出時,需自動刪除。<br>
     * 3. 當URL設置了category=routers時,表示分類存儲,缺省類別爲providers,可按分類部分通知數據。<br>
     * 4. 當註冊中心重啓,網絡抖動,不能丟失數據,包括斷線自動刪除數據。<br>
     * 5. 允許URI相同但參數不同的URL並存,不能覆蓋。<br>
     */
    void register(URL url);


    /**
     * 取消註冊.
     * <p>
     * 取消註冊需處理契約:<br>
     * 1. 如果是dynamic=false的持久存儲數據,找不到註冊數據,則拋IllegalStateException,否則忽略。<br>
     * 2. 按全URL匹配取消註冊。<br>
     */
    void unregister(URL url);

    /**
     * 訂閱符合條件的已註冊數據,當有註冊數據變更時自動推送.
     * <p>
     * 訂閱需處理契約:<br>
     * 1. 當URL設置了check=false時,訂閱失敗後不報錯,在後臺定時重試。<br>
     * 2. 當URL設置了category=routers,只通知指定分類的數據,多個分類用逗號分隔,並允許星號通配,表示訂閱所有分類數據。<br>
     * 3. 允許以interface,group,version,classifier作爲條件查詢,如:interface=com.alibaba.foo.BarService&version=1.0.0<br>
     * 4. 並且查詢條件允許星號通配,訂閱所有接口的所有分組的所有版本,或:interface=*&group=*&version=*&classifier=*<br>
     * 5. 當註冊中心重啓,網絡抖動,需自動恢復訂閱請求。<br>
     * 6. 允許URI相同但參數不同的URL並存,不能覆蓋。<br>
     * 7. 必須阻塞訂閱過程,等第一次通知完後再返回。<br>
     */
    void subscribe(URL url, NotifyListener listener);

    /**
     * 取消訂閱.
     * <p>
     * 取消訂閱需處理契約:<br>
     * 1. 如果沒有訂閱,直接忽略。<br>
     * 2. 按全URL匹配取消訂閱。<br>
     */
    void unsubscribe(URL url, NotifyListener listener);


    /**
     * 查詢符合條件的已註冊數據,與訂閱的推模式相對應,這裏爲拉模式,只返回一次結果。
     */
    List<URL> lookup(URL url);

2.4 Registry

註冊中心接口。Registry 繼承了RegistryService 和Node 接口

/**
 * 註冊中心接口
 */
public interface Registry extends Node, RegistryService {
}

2.5 AbstractRegistry

實現 Registry 接口,Registry 抽象類

2.5.1 屬性

// URL地址分隔符,用於文件緩存中,服務提供者URL分隔
    // URL address separator, used in file cache, service provider URL separation
    private static final char URL_SEPARATOR = ' ';
    // URL地址分隔正則表達式,用於解析文件緩存中服務提供者URL列表
    // URL address separated regular expression for parsing the service provider URL list in the file cache
    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} 服務提供者列表
     */
    // Local disk cache, where the special key value.registies records the list of registry centers, and the others are the list of notified service providers
    private final Properties properties = new Properties();
    /**
     * 註冊中心緩存寫入執行器。
     *
     * 線程數=1
     */
    // File cache timing writing
    private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
    /**
     * 是否同步保存文件
     */
    // Is it synchronized to save the file
    private final boolean syncSaveFile;
    /**
     * 數據版本號
     *
     * {@link #properties}
     */
    private final AtomicLong lastCacheChanged = new AtomicLong();
    /**
     * 已註冊 URL 集合。
     *
     * 注意,註冊的 URL 不僅僅可以是服務提供者的,也可以是服務消費者的
     */
    private final Set<URL> registered = new ConcurrentHashSet<URL>();
    /**
     * 訂閱 URL 的監聽器集合
     *
     * key:訂閱者的 URL ,例如消費者的 URL
     */
    private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    /**
     * 被通知的 URL 集合
     *
     * key1:消費者的 URL ,例如消費者的 URL ,和 {@link #subscribed} 的鍵一致
     * key2:分類,例如:providers、consumers、routes、configurators。【實際無 consumers ,因爲消費者不會去訂閱另外的消費者的列表】
     *            在 {@link Constants} 中,以 "_CATEGORY" 結尾
     */
    private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();
    /**
     * 註冊中心 URL
     */
    private URL registryUrl;
    /**
     * 本地磁盤緩存文件,緩存註冊中心的數據
     */
    // Local disk cache file
    private File file;
    /**
     * 是否銷燬
     */
    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()); 
    }

2.5.2register && unregister

註冊和移除 註冊中心url

   @Override
    public void register(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("register url == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Register: " + url);
        }
        // 添加到 registered 集合
        registered.add(url);
    }

    @Override
    public void unregister(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("unregister url == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Unregister: " + url);
        }
        // 移除出 registered 集合
        registered.remove(url);
    }

2.5.3 subscribe && unsubscribe

訂閱和取消訂閱註冊中 url

@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);
        }
        // 添加到 subscribed 集合
        Set<NotifyListener> listeners = subscribed.get(url);
        if (listeners == null) {
            subscribed.putIfAbsent(url, new ConcurrentHashSet<NotifyListener>());
            listeners = subscribed.get(url);
        }
        listeners.add(listener);
    }

    @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);
        }
        // 移除出 subscribed 集合
        Set<NotifyListener> listeners = subscribed.get(url);
        if (listeners != null) {
            listeners.remove(listener);
        }
    }

2.5.4 notify

第一,向註冊中心發起訂閱後,會獲取到全量數據,此時會被調用 #notify(…) 方法,即 Registry 獲取到了全量數據。
第二,每次註冊中心發生變更時,會調用 #notify(…) 方法,雖然變化是增量,調用這個方法的調用方,已經進行處理,傳入的 urls 依然是全量的。

 /**
     * 通知監聽器,URL 變化結果。
     *
     * @param urls 通知的 URL 變化結果(全量數據)
     */
    protected void notify(List<URL> urls) {
        if (urls == null || urls.isEmpty()) return;
        // 循環 `subscribed` ,通知監聽器們
        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);
                    }
                }
            }
        }
    }

2.6 FailbackRegistry

實現 AbstractRegistry 抽象類,支持失敗重試的 Registry 抽象類。

在上文中的代碼中,我們可以看到,AbstractRegistry 進行的註冊、訂閱等操作,更多的是修改狀態,而無和註冊中心實際的操作。FailbackRegistry 在 AbstractRegistry 的基礎上,實現了和註冊中心實際的操作,並且支持失敗重試的特性。

2.6.1 屬性

  /**
     * 定時任務執行器
     */
    // Scheduled executor service
    private final ScheduledExecutorService retryExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryFailedRetryTimer", true));

    /**
     * 失敗重試定時器,定時檢查是否有請求失敗,如有,無限次重試
     */
    // Timer for failure retry, regular check if there is a request for failure, and if there is, an unlimited retry
    private final ScheduledFuture<?> retryFuture;
    /**
     * 失敗發起註冊失敗的 URL 集合
     */
    private final Set<URL> failedRegistered = new ConcurrentHashSet<URL>();
    /**
     * 失敗取消註冊失敗的 URL 集合
     */
    private final Set<URL> failedUnregistered = new ConcurrentHashSet<URL>();
    /**
     * 失敗發起訂閱失敗的監聽器集合
     */
    private final ConcurrentMap<URL, Set<NotifyListener>> failedSubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    /**
     * 失敗取消訂閱失敗的監聽器集合
     */
    private final ConcurrentMap<URL, Set<NotifyListener>> failedUnsubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    /**
     * 失敗通知通知的 URL 集合
     */
    private final ConcurrentMap<URL, Map<NotifyListener, List<URL>>> failedNotified = new ConcurrentHashMap<URL, Map<NotifyListener, List<URL>>>();

    /**
     * 是否銷燬
     */
    private AtomicBoolean destroyed = new AtomicBoolean(false);

    public FailbackRegistry(URL url) {
        super(url);
        // 重試頻率,單位:毫秒
        int retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
        // 創建失敗重試定時器
        this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
            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);
    }

2.7 NotifyListener

通知監聽器。當收到服務變更通知時觸發,代碼如下

public interface NotifyListener {

    /**
     * 當收到服務變更通知時觸發。
     * <p>
     * 通知需處理契約:<br>
     * 1. 總是以服務接口和數據類型爲維度全量通知,即不會通知一個服務的同類型的部分數據,用戶不需要對比上一次通知結果。<br>
     * 2. 訂閱時的第一次通知,必須是一個服務的所有類型數據的全量通知。<br>
     * 3. 中途變更時,允許不同類型的數據分開通知,比如:providers, consumers, routers, overrides,允許只通知其中一種類型,但該類型的數據必須是全量的,不是增量的。<br>
     * 4. 如果一種類型的數據爲空,需通知一個empty協議並帶category參數的標識性URL數據。<br>
     * 5. 通知者(即註冊中心實現)需保證通知的順序,比如:單線程推送,隊列串行化,帶版本對比。<br>
     *
     * @param urls 已註冊信息列表,總不爲空,含義同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
     */
    void notify(List<URL> urls);

}

2.8 ProviderConsumerRegTable

服務提供者和消費者註冊表,存儲 JVM 進程內自己的服務提供者和消費者的 Invoker

/**
     * 服務提供者 Invoker 集合
     *
     * key:服務提供者 URL 服務鍵
     */
    public static ConcurrentHashMap<String, Set<ProviderInvokerWrapper>> providerInvokers = new ConcurrentHashMap<String, Set<ProviderInvokerWrapper>>();
    /**
     * 服務消費者 Invoker 集合
     *
     * key:服務消費者 URL 服務鍵
     */
    public static ConcurrentHashMap<String, Set<ConsumerInvokerWrapper>> consumerInvokers = new ConcurrentHashMap<String, Set<ConsumerInvokerWrapper>>();

2.8.1 ProviderInvokerWrapper

實現 Invoker 接口,服務提供者 Invoker Wrapper ,代碼如下:

   /**
     * Invoker 對象
     */
    private Invoker<T> invoker;
    /**
     * 原始 URL
     */
    private URL originUrl;
    /**
     * 註冊中心 URL
     */
    private URL registryUrl;
    /**
     * 服務提供者 URL
     */
    private URL providerUrl;
    /**
     * 是否註冊
     */
    private volatile boolean isReg;

2.8.2 ConsumerInvokerWrapper

/**
     * Invoker 對象
     */
    private Invoker<T> invoker;
    /**
     * 原始 URL
     */
    private URL originUrl;
    /**
     * 註冊中心 URL
     */
    private URL registryUrl;
    /**
     * 消費者 URL
     */
    private URL consumerUrl;
    /**
     * 註冊中心 Directory
     */
    private RegistryDirectory registryDirectory;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章