前面文章分析了服务的导出与引用过程,从本篇文章开始,我将开始分析 Dubbo 集群容错方面的源码。这部分源码包含四个部分,分别是服务目录 Directory、服务路由 Router、集群 Cluster 和负载均衡 LoadBalance。这几个部分的源码逻辑比较独立,我会分四篇文章进行分析。本篇文章做为集群容错的开篇文章,将和你们一块儿分析服务目录相关的源码。在进行深刻分析以前,咱们先来了解一下服务目录是什么。服务目录中存储了一些和服务提供者有关的信息,经过服务目录,服务消费者可获取到服务提供者的信息,好比 ip、端口、服务协议等。经过这些信息,服务消费者就可经过 Netty 等客户端进行远程调用。在一个服务集群中,服务提供者数量并非一成不变的,若是集群中新增了一台机器,相应地在服务目录中就要新增一条服务提供者记录。或者,若是服务提供者的配置修改了,服务目录中的记录也要作相应的更新。若是这样说,服务目录和注册中心的功能不就雷同了吗。确实如此,这里这么说是为了方便你们理解。实际上服务目录在获取注册中心的服务配置信息后,会为每条配置信息生成一个 Invoker 对象,并把这个 Invoker 对象存储起来,这个 Invoker 才是服务目录最终持有的对象。Invoker 有什么用呢?看名字就知道了,这是一个具备远程调用功能的对象。讲到这你们应该知道了什么是服务目录了,它能够看作是 Invoker 集合,且这个集合中的元素会随注册中心的变化而进行动态调整。html
好了,关于服务目录这里就先介绍这些,你们先有个大体印象便可。接下来咱们经过继承体系图来了解一下服务目录的家族成员都有哪些。java
服务目录目前内置的实现有两个,分别为 StaticDirectory 和 RegistryDirectory,它们均是 AbstractDirectory 的子类。AbstractDirectory 实现了 Directory 接口,这个接口包含了一个重要的方法定义,即 list(Invocation),用于列举 Invoker。下面咱们来看一下他们的继承体系图。git
如上,Directory 继承自 Node 接口,Node 这个接口继承者比较多,像 Registry、Monitor、Invoker 等继承了这个接口。这个接口包含了一个获取配置信息的方法 getUrl,实现该接口的类能够向外提供配置信息。另外,你们注意看 RegistryDirectory 实现了 NotifyListener 接口,当注册中心节点信息发生变化后,RegistryDirectory 能够经过此接口方法获得变动信息,并根据变动信息动态调整内部 Invoker 列表。github
如今你们对服务目录的继承体系应该比较清楚了,下面咱们深刻到源码中,探索服务目录是如何实现的。apache
本章我将分析 AbstractDirectory 和它两个子类的源码。这里之因此要分析 AbstractDirectory,而不是直接分析子类是有必定缘由的。AbstractDirectory 封装了 Invoker 列举流程,具体的列举逻辑则由子类实现,这是典型的模板模式。因此,接下来咱们先来看一下 AbstractDirectory 的源码。数组
public List<Invoker<T>> list(Invocation invocation) throws RpcException { if (destroyed) { throw new RpcException("Directory already destroyed..."); } // 调用 doList 方法列举 Invoker,这里的 doList 是模板方法,由子类实现 List<Invoker<T>> invokers = doList(invocation); // 获取路由器 List<Router> localRouters = this.routers; if (localRouters != null && !localRouters.isEmpty()) { for (Router router : localRouters) { try { // 获取 runtime 参数,并根据参数决定是否进行路由 if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) { // 进行服务路由 invokers = router.route(invokers, getConsumerUrl(), invocation); } } catch (Throwable t) { logger.error("Failed to execute router: ..."); } } } return invokers; } // 模板方法,由子类实现 protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException;
上面就是 AbstractDirectory 的 list 方法源码,这个方法封装了 Invoker 的列举过程。以下:缓存
以上步骤中,doList 是模板方法,需由子类实现。Router 的 runtime 参数这里简单说明一下,这个参数决定了是否在每次调用服务时都执行路由规则。若是 runtime 为 true,那么每次调用服务前,都须要进行服务路由。这个对性能形成影响,慎重配置。关于该参数更详细的说明,请参考官方文档。负载均衡
介绍完 AbstractDirectory,接下来咱们开始分析子类的源码。ide
StaticDirectory 即静态服务目录,顾名思义,它内部存放的 Invoker 是不会变更的。因此,理论上它和不可变 List 的功能很类似。下面咱们来看一下这个类的实现。源码分析
public class StaticDirectory<T> extends AbstractDirectory<T> { // Invoker 列表 private final List<Invoker<T>> invokers; // 省略构造方法 @Override public Class<T> getInterface() { // 获取接口类 return invokers.get(0).getInterface(); } // 检测服务目录是否可用 @Override public boolean isAvailable() { if (isDestroyed()) { return false; } for (Invoker<T> invoker : invokers) { if (invoker.isAvailable()) { // 只要有一个 Invoker 是可用的,就职务当前目录是可用的 return true; } } return false; } @Override public void destroy() { if (isDestroyed()) { return; } // 调用父类销毁逻辑 super.destroy(); // 遍历 Invoker 列表,并执行相应的销毁逻辑 for (Invoker<T> invoker : invokers) { invoker.destroy(); } invokers.clear(); } @Override protected List<Invoker<T>> doList(Invocation invocation) throws RpcException { // 列举 Inovker,也就是直接返回 invokers 成员变量 return invokers; } }
以上就是 StaticDirectory 的代码逻辑,很简单,你们都能看懂,我就很少说了。下面来看看 RegistryDirectory,这个类的逻辑比较复杂。
RegistryDirectory 是一种动态服务目录,它实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。收到变动通知后,RegistryDirectory 可根据配置变动信息刷新 Invoker 列表。RegistryDirectory 中有几个比较重要的逻辑,第一是 Invoker 的列举逻辑,第二是接受服务配置变动的逻辑,第三是 Invoker 的刷新逻辑。接下来,我将按顺序对这三块逻辑。
Invoker 列举逻辑封装在 doList 方法中,这是个模板方法,前面已经介绍过了。那这里就不过多啰嗦了,咱们直入主题吧。
public List<Invoker<T>> doList(Invocation invocation) { if (forbidden) { // 服务提供者关闭或禁用了服务,此时抛出 No provider 异常 throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry ..."); } List<Invoker<T>> invokers = null; // 获取 Invoker 本地缓存 Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) { // 获取方法名和参数列表 String methodName = RpcUtils.getMethodName(invocation); Object[] args = RpcUtils.getArguments(invocation); // 检测参数列表的第一个参数是否为 String 或 enum 类型 if (args != null && args.length > 0 && args[0] != null && (args[0] instanceof String || args[0].getClass().isEnum())) { // 经过 方法名 + 第一个参数名称 查询 Invoker 列表,具体的使用场景暂时没想到 invokers = localMethodInvokerMap.get(methodName + "." + args[0]); } if (invokers == null) { // 经过方法名获取 Invoker 列表 invokers = localMethodInvokerMap.get(methodName); } if (invokers == null) { // 经过星号 * 获取 Invoker 列表 invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); } if (invokers == null) { Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator(); if (iterator.hasNext()) { // 经过迭代器获取 Invoker 列表 invokers = iterator.next(); } } } // 返回 Invoker 列表 return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; }
以上代码进行屡次尝试,以期从 localMethodInvokerMap 中获取到 Invoker 列表。通常状况下,普通的调用可经过方法名获取到对应的 Invoker 列表,泛化调用可经过 * 获取到 Invoker 列表。按现有的逻辑,无论什么状况下,* 到 Invoker 列表的映射关系 <*, invokers> 老是存在的,也就意味着 localMethodInvokerMap.get(Constants.ANY_VALUE) 老是有值返回。除非这个值是 null,才会经过经过迭代器获取 Invoker 列表。至于什么状况下为空,我暂时未彻底搞清楚,我猜想是被路由规则(用户可基于 Router 接口实现自定义路由器)处理后,可能会获得一个 null。目前仅是猜想,未作验证。
本节的逻辑主要是从 localMethodInvokerMap 中获取 Invoker,localMethodInvokerMap 源自 RegistryDirectory 类的成员变量 methodInvokerMap。doList 方法能够看作是对 methodInvokerMap 变量的读操做,至于对 methodInvokerMap 变量的写操做,这个将在后续进行分析。
RegistryDirectory 是一个动态服务目录,它须要接受注册中心配置进行动态调整。所以 RegistryDirectory 实现了 NotifyListener 接口,经过这个接口获取注册中心变动通知。下面咱们来看一下具体的逻辑。
public synchronized void notify(List<URL> urls) { // 定义三个集合,分别用于存放服务提供者 url,路由 url,配置器 url List<URL> invokerUrls = new ArrayList<URL>(); List<URL> routerUrls = new ArrayList<URL>(); List<URL> configuratorUrls = new ArrayList<URL>(); for (URL url : urls) { String protocol = url.getProtocol(); // 获取 category 参数 String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY); // 根据 category 参数将 url 分别放到不一样的列表中 if (Constants.ROUTERS_CATEGORY.equals(category) || Constants.ROUTE_PROTOCOL.equals(protocol)) { // 添加路由器 url routerUrls.add(url); } else if (Constants.CONFIGURATORS_CATEGORY.equals(category) || Constants.OVERRIDE_PROTOCOL.equals(protocol)) { // 添加配置器 url configuratorUrls.add(url); } else if (Constants.PROVIDERS_CATEGORY.equals(category)) { // 添加服务提供者 url invokerUrls.add(url); } else { // 忽略不支持的 category logger.warn("Unsupported category ..."); } } if (configuratorUrls != null && !configuratorUrls.isEmpty()) { // 将 url 转成 Configurator this.configurators = toConfigurators(configuratorUrls); } if (routerUrls != null && !routerUrls.isEmpty()) { // 将 url 转成 Router List<Router> routers = toRouters(routerUrls); if (routers != null) { setRouters(routers); } } List<Configurator> localConfigurators = this.configurators; this.overrideDirectoryUrl = directoryUrl; if (localConfigurators != null && !localConfigurators.isEmpty()) { for (Configurator configurator : localConfigurators) { // 配置 overrideDirectoryUrl this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl); } } // 刷新 Invoker 列表 refreshInvoker(invokerUrls); }
如上,notify 方法首先是根据 url 的 category 参数对 url 进行分门别类存储,而后经过 toRouters 和 toConfigurators 将 url 列表转成 Router 和 Configurator 列表。最后调用 refreshInvoker 方法刷新 Invoker 列表。这里的 toRouters 和 toConfigurators 方法逻辑不复杂,你们自行分析。接下来,咱们把重点放在 refreshInvoker 方法上。
接着上一节继续分析,refreshInvoker 方法是保证 RegistryDirectory 随注册中心变化而变化的关键所在。这一块逻辑比较多,接下来一一进行分析。
private void refreshInvoker(List<URL> invokerUrls) { // invokerUrls 仅有一个元素,且 url 协议头为 empty,此时表示禁用全部服务 if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { // 设置 forbidden 为 true this.forbidden = true; this.methodInvokerMap = null; // 销毁全部 Invoker destroyAllInvokers(); } else { this.forbidden = false; Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) { // 添加缓存 url 到 invokerUrls 中 invokerUrls.addAll(this.cachedInvokerUrls); } else { this.cachedInvokerUrls = new HashSet<URL>(); // 缓存 invokerUrls this.cachedInvokerUrls.addAll(invokerUrls); } if (invokerUrls.isEmpty()) { return; } // 将 url 转成 Invoker Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls); // 将 newUrlInvokerMap 转成方法名到 Invoker 列表的映射 Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // 转换出错,直接打印异常,并返回 if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) { logger.error(new IllegalStateException("urls to invokers error ...")); return; } // 合并多个组的 Invoker this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; // 保存为本地缓存 this.urlInvokerMap = newUrlInvokerMap; try { // 销毁无用 Invoker destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); } catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } }
上面方法的代码不是不少,可是逻辑却很多。首先时根据入参 invokerUrls 的数量和协议头判断是否禁用全部的服务,若是禁用,则将 forbidden 设为 true,并销毁全部的 Invoker。若不由用,则将 url 转成 Invoker,获得 <url, Invoker> 的映射关系。而后进一步进行转换,获得 <methodName, Invoker 列表>。以后进行多组 Invoker 合并操做,并将合并结果赋值给 methodInvokerMap。methodInvokerMap 变量在 doList 方法中会被用到,doList 会对该变量进行读操做,在这里是写操做。当新的 Invoker 列表生成后,还要一个重要的工做要作,就是销毁无用的 Invoker,避免服务消费者调用已下线的服务的服务。
接下里,我将对上面涉及到的调用进行分析。按照顺序,这里先来分析 url 到 Invoker 的转换过程。
private Map<String, Invoker<T>> toInvokers(List<URL> urls) { Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>(); if (urls == null || urls.isEmpty()) { return newUrlInvokerMap; } Set<String> keys = new HashSet<String>(); // 获取服务消费端配置的协议 String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY); for (URL providerUrl : urls) { if (queryProtocols != null && queryProtocols.length() > 0) { boolean accept = false; String[] acceptProtocols = queryProtocols.split(","); // 检测服务提供者协议是否被服务消费者所支持 for (String acceptProtocol : acceptProtocols) { if (providerUrl.getProtocol().equals(acceptProtocol)) { accept = true; break; } } if (!accept) { // 若服务消费者协议头不被消费者所支持,则忽略当前 providerUrl continue; } } // 忽略 empty 协议 if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) { continue; } // 经过 SPI 检测服务端协议是否被消费端支持 if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) { logger.error(new IllegalStateException("Unsupported protocol...")); continue; } // 合并 url URL url = mergeUrl(providerUrl); String key = url.toFullString(); if (keys.contains(key)) { // 忽略重复 url continue; } keys.add(key); // 本地 Invoker 缓存列表 Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key); // 缓存未命中 if (invoker == null) { try { boolean enabled = true; if (url.hasParameter(Constants.DISABLED_KEY)) { // 获取 disable 配置,并修改 enable 变量 enabled = !url.getParameter(Constants.DISABLED_KEY, false); } else { enabled = url.getParameter(Constants.ENABLED_KEY, true); } if (enabled) { // 调用 refer 获取 Invoker invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl); } } catch (Throwable t) { logger.error("Failed to refer invoker for interface..."); } if (invoker != null) { // 缓存 Invoker 实例 newUrlInvokerMap.put(key, invoker); } } else { // 缓存命中,将 invoker 存储到 newUrlInvokerMap 中 newUrlInvokerMap.put(key, invoker); } } keys.clear(); return newUrlInvokerMap; }
toInvokers 方法一开始会对服务提供者 url 进行检测,若服务消费端的配置不支持服务端的协议,或服务端 url 协议头为 empty 时,toInvokers 均会忽略服务提供方 url。必要的检测作完后,紧接着是合并 url,而后访问缓存,尝试获取与 url 对应的 invoker。若是缓存命中,直接将 Invoker 存入 newUrlInvokerMap 中便可。若是未命中,则须要新建 Invoker。Invoker 是经过 Protocol 的 refer 方法建立的,这个我在上一篇文章中已经分析过了,这里就不赘述了。
toInvokers 方法返回的是 <url, Invoker> 映射关系表,接下来还要对这个结果进行进一步处理,获得方法名到 Invoker 列表的映射关系。这个过程由 toMethodInvokers 方法完成,以下:
private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) { // 方法名 -> Invoker 列表 Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>(); List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>(); if (invokersMap != null && invokersMap.size() > 0) { for (Invoker<T> invoker : invokersMap.values()) { // 获取 methods 参数 String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY); if (parameter != null && parameter.length() > 0) { // 切分 methods 参数值,获得方法名数组 String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter); if (methods != null && methods.length > 0) { for (String method : methods) { // 方法名不为 * if (method != null && method.length() > 0 && !Constants.ANY_VALUE.equals(method)) { // 根据方法名获取 Invoker 列表 List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null) { methodInvokers = new ArrayList<Invoker<T>>(); newMethodInvokerMap.put(method, methodInvokers); } // 存储 Invoker 到列表中 methodInvokers.add(invoker); } } } } invokersList.add(invoker); } } // 进行服务级别路由,参考:https://github.com/apache/incubator-dubbo/pull/749 List<Invoker<T>> newInvokersList = route(invokersList, null); // 存储 <*, newInvokersList> 映射关系 newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList); if (serviceMethods != null && serviceMethods.length > 0) { for (String method : serviceMethods) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); if (methodInvokers == null || methodInvokers.isEmpty()) { methodInvokers = newInvokersList; } // 进行方法级别路由 newMethodInvokerMap.put(method, route(methodInvokers, method)); } } // 排序,转成不可变列表 for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) { List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method); Collections.sort(methodInvokers, InvokerComparator.getComparator()); newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers)); } return Collections.unmodifiableMap(newMethodInvokerMap); }
上面方法主要作了三件事情, 第一是对入参进行遍历,而后获取 methods 参数,并切分红数组。随后以方法名为键,Invoker 列表为值,将映射关系存储到 newMethodInvokerMap 中。第二是分别基于类和方法对 Invoker 列表进行路由操做。第三是对 Invoker 列表进行排序,并转成不可变列表。关于 toMethodInvokers 方法就先分析到这,咱们继续向下分析,此次要分析的多组服务的合并逻辑。
private Map<String, List<Invoker<T>>> toMergeMethodInvokerMap(Map<String, List<Invoker<T>>> methodMap) { Map<String, List<Invoker<T>>> result = new HashMap<String, List<Invoker<T>>>(); // 遍历入参 for (Map.Entry<String, List<Invoker<T>>> entry : methodMap.entrySet()) { String method = entry.getKey(); List<Invoker<T>> invokers = entry.getValue(); // group -> Invoker 列表 Map<String, List<Invoker<T>>> groupMap = new HashMap<String, List<Invoker<T>>>(); // 遍历 Invoker 列表 for (Invoker<T> invoker : invokers) { // 获取分组配置 String group = invoker.getUrl().getParameter(Constants.GROUP_KEY, ""); List<Invoker<T>> groupInvokers = groupMap.get(group); if (groupInvokers == null) { groupInvokers = new ArrayList<Invoker<T>>(); // 缓存 <group, List<Invoker>> 到 groupMap 中 groupMap.put(group, groupInvokers); } // 存储 invoker 到 groupInvokers groupInvokers.add(invoker); } if (groupMap.size() == 1) { // 若是 groupMap 中仅包含一组键值对,此时直接取出该键值对的值便可 result.put(method, groupMap.values().iterator().next()); // groupMap 中包含多组键值对,好比: // { // "dubbo": [invoker1, invoker2, invoker3, ...], // "hello": [invoker4, invoker5, invoker6, ...] // } } else if (groupMap.size() > 1) { List<Invoker<T>> groupInvokers = new ArrayList<Invoker<T>>(); for (List<Invoker<T>> groupList : groupMap.values()) { // 经过集群类合并每一个分组对应的 Invoker 列表 groupInvokers.add(cluster.join(new StaticDirectory<T>(groupList))); } // 缓存结果 result.put(method, groupInvokers); } else { result.put(method, invokers); } } return result; }
上面方法首先是生成 group 到 Invoker 类比的映射关系表,若关系表中的映射关系数量大于1,表示有多组服务。此时经过集群类合并每组 Invoker,并将合并结果存储到 groupInvokers 中。以后将方法名与 groupInvokers 存到到 result 中,并返回,整个逻辑结束。
接下来咱们再来看一下 Invoker 列表刷新逻辑的最后一个动做 -- 删除无用 Invoker。以下:
private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) { if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) { destroyAllInvokers(); return; } List<String> deleted = null; if (oldUrlInvokerMap != null) { // 获取新生成的 Invoker 列表 Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values(); // 遍历老的 <url, Invoker> 映射表 for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) { // 检测 newInvokers 中是否包含老的 Invoker if (!newInvokers.contains(entry.getValue())) { if (deleted == null) { deleted = new ArrayList<String>(); } // 若不包含,则将老的 Invoker 对应的 url 存入 deleted 列表中 deleted.add(entry.getKey()); } } } if (deleted != null) { // 遍历 deleted 集合,并到老的 <url, Invoker> 映射关系表查出 Invoker,销毁之 for (String url : deleted) { if (url != null) { // 从 oldUrlInvokerMap 中移除 url 对应的 Invoker Invoker<T> invoker = oldUrlInvokerMap.remove(url); if (invoker != null) { try { // 销毁 Invoker invoker.destroy(); } catch (Exception e) { logger.warn("destroy invoker..."); } } } } } }
destroyUnusedInvokers 方法的主要逻辑是经过 newUrlInvokerMap 找出待删除 Invoker 对应的 url,并将 url 存入到 deleted 列表中。而后再遍历 deleted 列表,并从 oldUrlInvokerMap 中移除相应的 Invoker,销毁之。整个逻辑大体如此,不是很难理解。
到此关于 Invoker 列表的刷新逻辑就分析了,这里对整个过程进行简单总结。以下:
Invoker 的刷新逻辑仍是比较复杂的,你们在看的过程当中多写点 demo 进行调试。好了,本节就到这。
本篇文章对 Dubbo 服务目录进行了较为详细的分析,篇幅主要集中在 RegistryDirectory 的源码分析上。分析下来,不禁得感叹,想让本地服务目录和注册中心保持一致仍是须要作不少事情的,并不简单。服务目录是 Dubbo 集群容错的一部分,也是比较基础的部分,因此你们务必搞懂。
好了,本篇文章就先到这了。感谢你们阅读。
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
做者:田小波
本文同步发布在个人我的博客:http://www.tianxiaobo.com
本做品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。