引導
本章主要介紹下AbstractRegistry、FailbackRegistry的作用和原始碼。
AbstractRegistry
首先,直接引出這個類的作用,該類主要把服務提供者資訊快取本地檔案上,檔案目錄是:當前使用者目錄下的/.dubbo/dubbo-registry-${application}-${hos}-${port}.cache。
在解讀原始碼前,先閱讀下AbstractRegistry類的成員變數,從成員變數中可以看到這個類是怎麼完成資料的本地化儲存的。
// URL 地址分隔符
private static final char URL_SEPARATOR = ' ';
//URL地址正規表示式,任何空白符
private static final String URL_SPLIT = "\\s+";
// 引數儲存到本地檔案的最大重試次數
private static final int MAX_RETRY_TIMES_SAVE_PROPERTIES = 3;
// 需要儲存的引數
private final Properties properties = new Properties();
// 儲存執行緒,可以看出是否非同步儲存
private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
// 是否同步儲存
private boolean syncSaveFile;
// 上一次儲存的版本,每次儲存更新+1
private final AtomicLong lastCacheChanged = new AtomicLong();
// 儲存重試的次數
private final AtomicInteger savePropertiesRetryTimes = new AtomicInteger();
// 服務註冊的URL儲存在這裡
private final Set<URL> registered = new ConcurrentHashSet<>();
// 訂閱的URL,key:消費端訂閱者URL,values: 通知監聽器
private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<>();
// 訂閱的URL,key:消費端訂閱者URL,values: Map ,key:服務提供者的名字(預設為providers,configurations,routers),和服務提供者URL
private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<>();
//當前註冊URL,於指定的註冊中心連線的URL
private URL registryUrl;
//本地檔案
private File file;
入口,建構函式
public AbstractRegistry(URL url) {
// 儲存與註冊中心連線的url.
setUrl(url);
//判斷是否需要快取本地檔案,預設需要,檔案地址
if (url.getParameter(REGISTRY__LOCAL_FILE_CACHE_ENABLED, true)) {
// Start file save timer
// 是否同步儲存,預設是非同步
syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
//檔名和路徑一般在,當前使用者目錄下的/.dubbo/dubbo-registry-${application}-${hos}-${port}.cache
//例如dubbo-registry-dubbo-demo-annotation-provider-106.52.187.48-2181.cache
String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(APPLICATION_KEY) + "-" + 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;
//在啟動訂閱中心時,我們需要讀取本地快取檔案,以便將來進行登錄檔容錯處理。 其實就是把本地檔案file的內容 放入引數properties裡
loadProperties();
// 進行通知url.getBackupUrls(),第一個引數就是url 自己本身
notify(url.getBackupUrls());
}
}
上面的註釋已經非常的清晰了,這裡就不在描述,需要關注的是notify()這個函式,所以當每個服務註冊和訂閱時,首次建立註冊中心都會進行notify操作。具體來看下notify方法。
protected void notify(List<URL> urls) {
// 這裡是註冊中心連結的url,裡面包括了服務提供方的資訊(key:interface等)
if (CollectionUtils.isEmpty(urls)) {
return;
}
// 這裡迴圈所有的訂閱URL
for (Map.Entry<URL, Set<NotifyListener>> entry : getSubscribed().entrySet()) {
URL url = entry.getKey();
// 檢視訂閱的url 是否是訂閱當前的註冊服務。不是的話,輪訓下一個
if (!UrlUtils.isMatch(url, urls.get(0))) {
continue;
}
// 這裡訂閱的URL的通知監聽器
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);
}
}
}
}
}
接下來看下具體的notify(URL url, NotifyListener listener, List
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 + ", urls: " + urls);
}
//result,key: providers,configurators,routers ,values:urls.
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) { // 這裡再一次判斷,訂閱URL 和服務提供者URL 是否匹配
String category = u.getParameter(CATEGORY_KEY, 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);
//監聽通知
listener.notify(categoryList);
// 我們將在每次通知後更新快取檔案。
// 當我們的登錄檔由於網路抖動而出現訂閱失敗時,我們至少可以返回現有的快取URL。
saveProperties(url);
}
}
接著看下saveProperties,
private void saveProperties(URL url) {
if (file == null) {
return;
}
try {
StringBuilder buf = new StringBuilder();
// 得到該訂閱URL 的所有服務提供者URLS,並放入buf中
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified != null) {
for (List<URL> us : categoryNotified.values()) {
for (URL u : us) {
if (buf.length() > 0) {
buf.append(URL_SEPARATOR);
}
buf.append(u.toFullString());
}
}
}
// key 服務介面,value :提供者URL.
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);
}
}
從上面可以知道,把消費端的訂閱的服務資訊存入了file檔案中,doSaveProperties就是檔案操作,不進行分析。再一次強調下,消費端訂閱時,會訂閱某個具體服務下3個節點(providers,configurations,routers)。
FailbackRegistry
接著,FailbackRegistry繼承自AbstractRegistry。
其建構函式如下,可以得知除了呼叫AbstractRegistry構造方法外,並且建立一個HashedWheelTimer型別的定時器。
public FailbackRegistry(URL url) {
super(url);
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);
}
並且FailbackRegistry 成員記錄一組註冊失敗和訂閱失敗的集合,然後通過retryTimer定式掃描這些失敗集合,重新發起訂閱和註冊。之後會單獨拿一章節來講解這個時間輪演算法。
下面是失敗集合:
// 這裡是註冊失敗的urls
private final ConcurrentMap<URL, FailedRegisteredTask> failedRegistered = new ConcurrentHashMap<URL, FailedRegisteredTask>();
// 這裡是取消註冊失敗的urls
private final ConcurrentMap<URL, FailedUnregisteredTask> failedUnregistered = new ConcurrentHashMap<URL, FailedUnregisteredTask>();
// 這裡是訂閱失敗的urls
private final ConcurrentMap<Holder, FailedSubscribedTask> failedSubscribed = new ConcurrentHashMap<Holder, FailedSubscribedTask>();
// 這裡是取消訂閱失敗的urls
private final ConcurrentMap<Holder, FailedUnsubscribedTask> failedUnsubscribed = new ConcurrentHashMap<Holder, FailedUnsubscribedTask>();
// 這裡是通知notify()方法失敗異常時的url集合,會進行重新通知
private final ConcurrentMap<Holder, FailedNotifiedTask> failedNotified = new ConcurrentHashMap<Holder, FailedNotifiedTask>();
本章的內容比較簡單,主要是接上一章節Dubbo系列之 (二)Registry註冊中心-註冊(1)的內容,使其完整。目前我們已經完成大部分dubbo是如何與註冊中心互動的,接下來的章節我們講繼續分享dubbo服務的匯出和訂閱等內容。