1 Dubbo注冊(cè)中心概述
Dubbo的注冊(cè)中心承擔(dān)著Dubbo服務(wù)的注冊(cè)與發(fā)現(xiàn)的功能燕偶。
Dubbo支持的注冊(cè)中心主要包括:
ZookeeperRegistry
MulticastRegistry
RedisRegistry
DubboRegistry
其中dubbo官方推薦用Zookeeper作為注冊(cè)中心贩耐,下面介紹ZookeeperRegistry
。
2 Dubbo服務(wù)注冊(cè)
2.1 Dubbo服務(wù)注冊(cè)組件
Dubbo在Registry層實(shí)現(xiàn)服務(wù)的注冊(cè)于發(fā)現(xiàn)醒叁,主要包括如下幾個(gè)類:
ZookeeperRegistry:負(fù)責(zé)與zookeeper進(jìn)行交互;
RegistryProtocol:從注冊(cè)中心獲取可用服務(wù),或者將服務(wù)注冊(cè)到zookeeper疫粥,然后提供服務(wù)或者提供調(diào)用代理蟆湖;
RegistryDirectory:維護(hù)著所有可用的遠(yuǎn)程
Invoker
或者本地的Invoker
故爵,這個(gè)類實(shí)現(xiàn)了NotifyListner
;NotifyListener :負(fù)責(zé)
RegistryDirectory
和ZookeeperRegistry
的通信隅津;FailbackRegistry:繼承自
Registry
诬垂,實(shí)現(xiàn)了失敗重試機(jī)制;
2.2 Zookeeper的服務(wù)注冊(cè)模型
流程說明:
-
服務(wù)提供者啟動(dòng)時(shí)
- 向
/dubbo/com.foo.BarService/providers
目錄下寫入自己的URL地址饥瓷。
- 向
-
服務(wù)消費(fèi)者啟動(dòng)時(shí)
訂閱
/dubbo/com.foo.BarService/providers
目錄下的提供者URL地址剥纷。并向
/dubbo/com.foo.BarService/consumers
目錄下寫入自己的URL地址。
-
監(jiān)控中心啟動(dòng)時(shí)
- 訂閱
/dubbo/com.foo.BarService
目錄下的所有提供者和消費(fèi)者URL地址呢铆。
- 訂閱
3 服務(wù)發(fā)布
RegistryProtocol
是對(duì)需要暴露服務(wù)到注冊(cè)中心的一層封裝晦鞋,通過RegistryProtocol
實(shí)現(xiàn)將暴露的服務(wù)信息注冊(cè)到注冊(cè)中心。
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 暴露服務(wù),然后包裝暴露者
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
// 獲取注冊(cè)中心地址
// 通過服務(wù)的registry配置悠垛,
//<dubbo:registry id="asRegistry" client="zkclient" protocol="zookeeper" register="true" address="zookeeper:2181" file="D:/dubbo/user/dubbo.registry"/>
//<dubbo:protocol name="dubbo" port="20880" dispatcher="direct" threadpool="fixed" threads="2"/>
//<dubbo:service connections="10" interface="com.wuhulala.dubbo.user.service.UserService" ref="demoService" registry="asRegistry"/>
URL registryUrl = getRegistryUrl(originInvoker);
//根據(jù)注冊(cè)URL 獲取注冊(cè)中心
final Registry registry = getRegistry(originInvoker);
// 處理需要注冊(cè)到注冊(cè)中心的地址
final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
//判斷是否需要注冊(cè)线定,為什么不放到最上面
boolean register = registedProviderUrl.getParameter("register", true);
ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);
if (register) {
// 注冊(cè)邏輯
register(registryUrl, registedProviderUrl);
ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}
// 省略 ...
}
public void register(URL registryUrl, URL registedProviderUrl) {
// registry在初始化的時(shí)候被注入一個(gè)自適應(yīng)的擴(kuò)展點(diǎn)ExtensionLoader.getExtensionLoader(type).getAdaptiveExtension()
//所以發(fā)布的時(shí)候也是自適應(yīng)尋找對(duì)應(yīng)擴(kuò)展點(diǎn)進(jìn)行注冊(cè)到注冊(cè)中心
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registedProviderUrl);
}
3 ZookeeperRegistry
ZookeeperRegistry
是通過ZookeeperRegistryFactory
創(chuàng)建。
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName())
.addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
String key = url.toServiceString();
// 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);
return registry;
} finally {
// Release the lock
LOCK.unlock();
}
}
說明:
上面是AbstractRegistryFactory#getRegistry
方法根據(jù)URL獲取對(duì)應(yīng)注冊(cè)中心确买,真正的創(chuàng)建在子類的createRegistry
方法中實(shí)現(xiàn)斤讥。
3.1 ZookeeperRegistryFactory
public Registry createRegistry(URL url) {
return new ZookeeperRegistry(url, zookeeperTransporter);
}
注意:
ZookeeperTransporter
才是真正與Zookeeper通信的對(duì)象。默認(rèn)的Zookeeper客戶端是curator湾趾。
3.2 ZookeeperRegistry的創(chuàng)建
/**
* 默認(rèn)zookeeper的跟節(jié)點(diǎn) dubbo
*/
private final static String DEFAULT_ROOT = "dubbo";
/**
* 根節(jié)點(diǎn)
*/
private final String root;
/**
* Service 接口全名集合芭商。
* 該屬性適可用于監(jiān)控中心,訂閱整個(gè)Service層搀缠。因?yàn)镾ervice層是動(dòng)態(tài)的铛楣,可以有不斷有新的Service服務(wù)發(fā)布(注意,不是服務(wù)實(shí)例)艺普。
* 在 #doSubscribe(url, notifyListener) 方法中簸州,
*/
private final Set<String> anyServices = new ConcurrentHashSet<>();
/**
* 監(jiān)聽器集合
*/
private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<>();
/**
* zookeeper客戶端
*/
private final ZookeeperClient zkClient;
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
//獲取根節(jié)點(diǎn),不設(shè)置則使用/dubbo
String group = url.getParameter(GROUP_KEY, DEFAULT_ROOT);
if (!group.startsWith(PATH_SEPARATOR)) {
group = PATH_SEPARATOR + group;
}
this.root = group;
//獲取zk客戶端調(diào)用 ZookeeperTransporter#connect(url) 方法歧譬,
// 基于 Dubbo SPIAdaptive 機(jī)制岸浑,根據(jù)url參數(shù),加載對(duì)應(yīng)的 ZookeeperTransporter 實(shí)現(xiàn)類瑰步,創(chuàng)建對(duì)應(yīng)的 ZookeeperClient 實(shí)現(xiàn)類的對(duì)應(yīng)矢洲。
zkClient = zookeeperTransporter.connect(url);
//設(shè)置監(jiān)聽器
zkClient.addStateListener((state) -> {
//重連,重新獲取最新的服務(wù)提供方地址信息
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) {
//重新創(chuàng)建session時(shí)缩焦,將本服務(wù)進(jìn)行recover恢復(fù)兵钮,重新發(fā)起注冊(cè)和訂閱
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) {
}
});
}
ZookeeperRegistry
在創(chuàng)建時(shí)便和Zookeeper創(chuàng)建了連接。
4 服務(wù)注冊(cè)
Zookeeper的服務(wù)注冊(cè)就是創(chuàng)建Zookeeper的節(jié)點(diǎn)舌界。
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);
}
}
根據(jù)url對(duì)象構(gòu)建一個(gè)URL的Path
掘譬,然后在Zookeeper上創(chuàng)建一個(gè)節(jié)點(diǎn),節(jié)點(diǎn)是否是持久化的根據(jù)dynamic
參數(shù)決定呻拌,true
表示創(chuàng)建一個(gè)持久化節(jié)點(diǎn)葱轩,當(dāng)注冊(cè)方下線時(shí),持久化節(jié)點(diǎn)仍然在Zookeeper上藐握。false
表示創(chuàng)建臨時(shí)節(jié)點(diǎn)靴拱,當(dāng)注冊(cè)方下線時(shí),會(huì)刪除節(jié)點(diǎn)猾普,同時(shí)通知監(jiān)聽節(jié)點(diǎn)的消費(fèi)方袜炕。
4.1 節(jié)點(diǎn)路徑
Zookeeper中的服務(wù)節(jié)點(diǎn)路徑如Group/ServiceInterface/category/URL
:
Group:表示節(jié)點(diǎn)的根路徑,默認(rèn)是dubbo初家;
ServiceInterface:接口的全限定類名偎窘;
category:接口的類別乌助,providers、consumers陌知、configurators他托、routers等這些;
URL:URL的編碼仆葡;
4.2 服務(wù)下線
服務(wù)下線就是把注冊(cè)的節(jié)點(diǎn)進(jìn)行刪除赏参。
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);
}
}
5 服務(wù)訂閱
訂閱有兩種方式pull、push 沿盅,pull是客戶端定時(shí)輪詢注冊(cè)中心拉取配置把篓,push是注冊(cè)中心主動(dòng)推送數(shù)據(jù)給客戶端。dubbo是兩者結(jié)合的方式進(jìn)行配置的拉取腰涧,當(dāng)?shù)谝淮螁?dòng)的時(shí)候通過客戶端pull拉取所有數(shù)據(jù)方式纸俭,在訂閱的節(jié)點(diǎn)上進(jìn)行注冊(cè)watcher,然后客戶端與注冊(cè)中心保持長(zhǎng)連接南窗,而后 每個(gè)節(jié)點(diǎn)有任何數(shù)據(jù)變化,注冊(cè)中心就會(huì)更新watcher情況進(jìn)行回調(diào)通知到客戶端郎楼,客戶端就接收到這個(gè)通知了万伤。
服務(wù)在進(jìn)行注冊(cè)的時(shí)候,服務(wù)端會(huì)訂閱configurators用來監(jiān)聽動(dòng)態(tài)配置的變更呜袁。
在消費(fèi)者啟動(dòng)的時(shí)候敌买,消費(fèi)者會(huì)監(jiān)聽providers、routers阶界、configurators來監(jiān)聽服務(wù)提供者虹钮、路由規(guī)則、配置變更的通知膘融。
5.1 doSubscribe
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
//處理所有service的訂閱,
//客戶端第一次連接上注冊(cè)中心的時(shí)候芙粱,會(huì)把這個(gè)service設(shè)置成* 也就是 ANY_VALUE,進(jìn)行獲取全量數(shù)據(jù)
if (ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
//獲取url的監(jiān)聽器
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
//沒有l(wèi)istener則創(chuàng)建一
zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
listeners = zkListeners.get(url);
}
//獲取ChildListener
ChildListener zkListener = listeners.get(listener);
//第一次連接時(shí)為空,新建一個(gè)childListener
if (zkListener == null) {
listeners.putIfAbsent(listener, (parentPath, currentChilds) -> {
//遍歷所有子節(jié)點(diǎn)
for (String child : currentChilds) {
child = URL.decode(child);
//有新增服務(wù)氧映,則發(fā)起這個(gè)服務(wù)的訂閱
if (!anyServices.contains(child)) {
anyServices.add(child);
subscribe(url.setPath(child).addParameters(INTERFACE_KEY, child,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
});
zkListener = listeners.get(listener);
}
//創(chuàng)建持久化節(jié)點(diǎn)春畔,訂閱持久化節(jié)點(diǎn)的子節(jié)點(diǎn)
zkClient.create(root, false);
List<String> services = zkClient.addChildListener(root, zkListener);
//獲取到全量service,對(duì)每一個(gè)service進(jìn)行訂閱
if (CollectionUtils.isNotEmpty(services)) {
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters(INTERFACE_KEY, service,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
} else {//客戶端非第一次連接上注冊(cè)中心岛都,對(duì)于具體的服務(wù)進(jìn)行訂閱
List<URL> urls = new ArrayList<>();
//根據(jù)URL類別律姨,獲取一組需要訂閱的路徑
//類別:providers、routers臼疫、consumers择份、configurators
for (String path : toCategoriesPath(url)) {
//獲取url對(duì)應(yīng)的監(jiān)聽器,沒有則創(chuàng)建
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
listeners = zkListeners.get(url);
}
//獲取ChildListener烫堤,沒有則創(chuàng)建
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
//子節(jié)點(diǎn)數(shù)據(jù)發(fā)生變更時(shí)則調(diào)用notify方法進(jìn)行通知這個(gè)listener
listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
zkListener = listeners.get(listener);
}
//創(chuàng)建type的持久化節(jié)點(diǎn)荣赶,且對(duì)于這個(gè)節(jié)點(diǎn)進(jìn)行訂閱
zkClient.create(path, false);
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
//數(shù)據(jù)獲取完成時(shí)凤价,調(diào)用 `#notify(...)` 方法,回調(diào) NotifyListener讯壶,urls為所有自節(jié)點(diǎn)的url
notify(url, listener, urls);
}
} catch (Throwable e) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
5.2 doUnsubscribe
public void doUnsubscribe(URL url, NotifyListener listener) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners != null) {
ChildListener zkListener = listeners.get(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);
}
}
}
}
}
取消訂閱就是刪除監(jiān)聽器料仗。
5.3 fetchLatestAddresses
當(dāng)與zookeeper重新連接時(shí),需要刷新本機(jī)生產(chǎn)者列表伏蚊,這個(gè)方法就是干這個(gè)的立轧,它把的已訂閱的列表都加入訂閱失敗的容器。
//刷新生產(chǎn)者列表
private void fetchLatestAddresses() {
// subscribe
Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
if (!recoverSubscribed.isEmpty()) {
if (logger.isInfoEnabled()) {
logger.info("Fetching the latest urls of " + recoverSubscribed.keySet());
}
for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
URL url = entry.getKey();
//將當(dāng)前訂閱的列表全部推送至訂閱失敗的容器
for (NotifyListener listener : entry.getValue()) {
addFailedSubscribed(url, listener);
}
}
}
}
注意:
addFailedSubscribed
方法是FailbackRegistry
將訂閱失敗的連接放入失敗容器躏吊,FailbackRegistry
有定時(shí)器會(huì)定時(shí)的處理這些失敗的訂閱氛改。
6 服務(wù)監(jiān)聽
通知事件是調(diào)用的AbstractRegistry#notify
方法完成。
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
//......
//將url分類放置
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
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;
}
//獲取url在notified的全部url數(shù)據(jù),若notified無則創(chuàng)建一個(gè)且放入
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);
//對(duì)于所有url進(jìn)行通知
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.
//當(dāng)我們的注冊(cè)表由于網(wǎng)絡(luò)抖動(dòng)而出現(xiàn)訂閱失敗時(shí)比伏,我們至少可以返回現(xiàn)有的緩存URL胜卤。
saveProperties(url);
}
}
流程說明:
- 對(duì)需要通知的url進(jìn)行分類;
- 分類調(diào)用notify方法進(jìn)行通知赁项,這個(gè)也就是
org.apache.dubbo.registry.NotifyListener#notify
這個(gè)方法葛躏,也就是在服務(wù)訂閱doSubscribe
傳入的參數(shù)NotifyListener
; - 將url進(jìn)行保存緩存悠菜;
6.1 NotifyListener通知
public interface NotifyListener {
/**
* 當(dāng)收到服務(wù)變更通知時(shí)觸發(fā)舰攒。
* <p>
* 通知需處理契約:<br>
* 1\. 總是以服務(wù)接口和數(shù)據(jù)類型為維度全量通知,即不會(huì)通知一個(gè)服務(wù)的同類型的部分?jǐn)?shù)據(jù)悔醋,用戶不需要對(duì)比上一次通知結(jié)果摩窃。<br>
* 2\. 訂閱時(shí)的第一次通知,必須是一個(gè)服務(wù)的所有類型數(shù)據(jù)的全量通知芬骄。<br>
* 3\. 中途變更時(shí)猾愿,允許不同類型的數(shù)據(jù)分開通知,比如:providers, consumers, routers, overrides账阻,允許只通知其中一種類型蒂秘,但該類型的數(shù)據(jù)必須是全量的,不是增量的淘太。<br>
* 4\. 如果一種類型的數(shù)據(jù)為空材彪,需通知一個(gè)empty協(xié)議并帶category參數(shù)的標(biāo)識(shí)性URL數(shù)據(jù)。<br>
* 5\. 通知者(即注冊(cè)中心實(shí)現(xiàn))需保證通知的順序琴儿,比如:?jiǎn)尉€程推送段化,隊(duì)列串行化,帶版本對(duì)比造成。<br>
*
* @param urls 已注冊(cè)信息列表显熏,總不為空,含義同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值晒屎。
*/
void notify(List<URL> urls);
}
RegistryDirectory#notify
就是更新本地緩存喘蟆。
6.2 saveProperties
private void saveProperties(URL url) {
if (file == null) {
return;
}
try {
StringBuilder buf = new StringBuilder();
/**
* notifies是 被通知的 URL 集合
*
* key1:消費(fèi)者的 URL 缓升,例如消費(fèi)者的 URL ,和 {@link #subscribed} 的鍵一致
* key2:分類蕴轨,例如:providers港谊、consumers、routes橙弱、configurators歧寺。【實(shí)際無 consumers 棘脐,因?yàn)橄M(fèi)者不會(huì)去訂閱另外的消費(fèi)者的列表】
* 在 {@link Constants} 中斜筐,以 "_CATEGORY" 結(jié)尾
*/
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());
}
}
}
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在AbstractRegistry
對(duì)象創(chuàng)建的時(shí)候初始化。
6.3 doSaveProperties
public void doSaveProperties(long version) {
if (version < lastCacheChanged.get()) {
return;
}
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);
}
}
這使用了文件鎖的方式保證只有一個(gè)在讀寫文件蛀缝。
消費(fèi)者從注冊(cè)中心獲取注冊(cè)信息后會(huì)做本地緩存顷链。內(nèi)存中也有一份,保存在Properties
對(duì)象里屈梁,磁盤上也持久化一份嗤练,通過file對(duì)象進(jìn)行引用。