Dubbo源码学习总结系列七---注册中心

        Dubbo注册中心是框架的核心模块,提供了服务注册发现(包括服务提供者、消费者、路由策略、覆盖规则)的功能,该功能集中体现了服务治理的特性。该模块结合Cluster模块实现了集群服务。Dubbo管理控制台查询注册的数据展现服务提供者、消费者、路由策略、覆盖规则相关信息。监控中心从注册中心订阅相关信息实时监控调用链调用情况。

        那么,Registry模块的职责我们总结为:

        (1)注册:包括服务提供者、路由策略、覆盖规则信息注册到注册中心;

        (2)订阅:消费端从注册中心订阅相关信息;

        (3)通知(push):注册信息有变化,注册中心向订阅者主动通知变更信息;

        (4)获得服务列表(pull):消费方主动从注册中心目录服务拉取服务提供者列表等信息。

        从以下几个架构图可以看出Registry在Dubbo框架中所扮演的角色,架构图摘自Dubbo开发者文档:

        (1)依赖关系图中1-3的步骤我们看到提供者和消费者对注册中心的依赖;

        

        (2)调用链图(一部分)显示了组件调用Registry的场景:消费端refer时,从注册中心目录服务获得服务提供者列表。

        (3)暴露服务时序图中,服务提供者向注册中心注册或取消注册服务提供者信息。

        (4)引用服务时序图中消费端配置指向RegistryProtocol,实现向注册中心订阅提供者信息,销毁时取消订阅。

        注册中心的配置方法如下:        

<!-- 定义注册中心 -->
<dubbo:registry id="xxx1" address="xxx://ip:port" />
<!-- 引用注册中心,如果没有配置registry属性,将在ApplicationContext中自动扫描registry配置 -->
<dubbo:service registry="xxx1" />
<!-- 引用注册中心缺省值,当<dubbo:service>没有配置registry属性时,使用此配置 -->
<dubbo:provider registry="xxx1" />

        定义的接口有:

        RegistryFactory.java:

 1 public interface RegistryFactory {
 2     /**
 3      * 连接注册中心.
 4      * 
 5      * 连接注册中心需处理契约:<br>
 6      * 1. 当设置check=false时表示不检查连接,否则在连接不上时抛出异常。<br>
 7      * 2. 支持URL上的username:password权限认证。<br>
 8      * 3. 支持backup=10.20.153.10备选注册中心集群地址。<br>
 9      * 4. 支持file=registry.cache本地磁盘文件缓存。<br>
10      * 5. 支持timeout=1000请求超时设置。<br>
11      * 6. 支持session=60000会话超时或过期设置。<br>
12      * 
13      * @param url 注册中心地址,不允许为空
14      * @return 注册中心引用,总不返回空
15      */
16     Registry getRegistry(URL url); 
17 }

   RegistryService.java:

 1 public interface RegistryService { // Registry extends RegistryService 
 2     /**
 3      * 注册服务.
 4      * 
 5      * 注册需处理契约:<br>
 6      * 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。<br>
 7      * 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。<br>
 8      * 3. 当URL设置了category=overrides时,表示分类存储,缺省类别为providers,可按分类部分通知数据。<br>
 9      * 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。<br>
10      * 5. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
11      * 
12      * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
13      */
14     void register(URL url);
15 
16     /**
17      * 取消注册服务.
18      * 
19      * 取消注册需处理契约:<br>
20      * 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。<br>
21      * 2. 按全URL匹配取消注册。<br>
22      * 
23      * @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
24      */
25     void unregister(URL url);
26 
27     /**
28      * 订阅服务.
29      * 
30      * 订阅需处理契约:<br>
31      * 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。<br>
32      * 2. 当URL设置了category=overrides,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。<br>
33      * 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0<br>
34      * 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*<br>
35      * 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。<br>
36      * 6. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
37      * 7. 必须阻塞订阅过程,等第一次通知完后再返回。<br>
38      * 
39      * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
40      * @param listener 变更事件监听器,不允许为空
41      */
42     void subscribe(URL url, NotifyListener listener);
43 
44     /**
45      * 取消订阅服务.
46      * 
47      * 取消订阅需处理契约:<br>
48      * 1. 如果没有订阅,直接忽略。<br>
49      * 2. 按全URL匹配取消订阅。<br>
50      * 
51      * @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
52      * @param listener 变更事件监听器,不允许为空
53      */
54     void unsubscribe(URL url, NotifyListener listener);
55 
56     /**
57      * 查询注册列表,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
58      * 
59      * @see com.alibaba.dubbo.registry.NotifyListener#notify(List)
60      * @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
61      * @return 已注册信息列表,可能为空,含义同{@link com.alibaba.dubbo.registry.NotifyListener#notify(List<URL>)}的参数。
62      */
63     List<URL> lookup(URL url);
64 
65 }

         NotifyListener.java:

 1 public interface NotifyListener { 
 2     /**
 3      * 当收到服务变更通知时触发。
 4      * 
 5      * 通知需处理契约:<br>
 6      * 1. 总是以服务接口和数据类型为维度全量通知,即不会通知一个服务的同类型的部分数据,用户不需要对比上一次通知结果。<br>
 7      * 2. 订阅时的第一次通知,必须是一个服务的所有类型数据的全量通知。<br>
 8      * 3. 中途变更时,允许不同类型的数据分开通知,比如:providers, consumers, routes, overrides,允许只通知其中一种类型,但该类型的数据必须是全量的,不是增量的。<br>
 9      * 4. 如果一种类型的数据为空,需通知一个empty协议并带category参数的标识性URL数据。<br>
10      * 5. 通知者(即注册中心实现)需保证通知的顺序,比如:单线程推送,队列串行化,带版本对比。<br>
11      * 
12      * @param urls 已注册信息列表,总不为空,含义同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
13      */
14     void notify(List<URL> urls);
15 
16 }

          Registry.java,集成了Node和RegistryService接口,定义了一个针对特定URL,有生命周期的注册中心接口:

1 /**
2  * Registry. (SPI, Prototype, ThreadSafe)
3  *
4  * @see com.alibaba.dubbo.registry.RegistryFactory#getRegistry(URL)
5  * @see com.alibaba.dubbo.registry.support.AbstractRegistry
6  */
7 public interface Registry extends Node, RegistryService {
8 }

         register模块在调用链中的位置是:消费端通过ClusterInvoker调用RegisterDirectory的list(url)方法得到Invoker列表,即从注册中心获得指定url的Invoker列表。服务提供者在注册中心注册了自己的服务信息,被消费端订阅获取。见下图红框中的红色箭头,即为调用过程。

        

        RegistoryDirectory.java为注册中心目录服务,继承了AbstractDirectory,实现doList(invokation)方法,它是属于消费端类。

        主要逻辑为:

        (1)从invokation中解析出要请求的URL;

        (2)根据配置中的protocol和Url查找缓存(HashMap,本地内存缓存)中的invokers,如果找到则返回,否则调用属性protocol(初始化时注入的实例,如DubboProtocol实例)的refer()方法得到invokers并返回;

 1     public List<Invoker<T>> doList(Invocation invocation) {
 2         if (forbidden) {
 3             // 1. No service provider 2. Service providers are disabled
 4             throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
 5                 "No provider available from registry " + getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +  NetUtils.getLocalHost()
 6                     + " use dubbo version " + Version.getVersion() + ", may be providers disabled or not registered ?");
 7         }
 8         List<Invoker<T>> invokers = null;
//invoker本地缓存,方法名作为key的invoker Map
9 Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference 10 if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) { 11 String methodName = RpcUtils.getMethodName(invocation); 12 Object[] args = RpcUtils.getArguments(invocation); 13 if (args != null && args.length > 0 && args[0] != null 14 && (args[0] instanceof String || args[0].getClass().isEnum())) { 15 invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // The routing can be enumerated according to the first parameter 16 } 17 if (invokers == null) { 18 invokers = localMethodInvokerMap.get(methodName); 19 } 20 if (invokers == null) { 21 invokers = localMethodInvokerMap.get(Constants.ANY_VALUE); 22 } 23 if (invokers == null) { 24 Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator(); 25 if (iterator.hasNext()) { 26 invokers = iterator.next(); 27 } 28 } 29 } 30 return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers; 31 }

         以下代码实现了根据url列表构造invoker的map:

 1 /**
 2      * Turn urls into invokers, and if url has been refer, will not re-reference.
 3      *
 4      * @param urls
 5      * @return invokers
 6      */
 7     private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
 8         Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
 9         if (urls == null || urls.size() == 0) {
10             return newUrlInvokerMap;
11         }
12         Set<String> keys = new HashSet<String>();
13         String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
14         for (URL providerUrl : urls) {
15             // If protocol is configured at the reference side, only the matching protocol is selected
16             if (queryProtocols != null && queryProtocols.length() > 0) {
17                 boolean accept = false;
18                 String[] acceptProtocols = queryProtocols.split(",");
19                 for (String acceptProtocol : acceptProtocols) {
20                     if (providerUrl.getProtocol().equals(acceptProtocol)) {
21                         accept = true;
22                         break;
23                     }
24                 }
25                 if (!accept) {
26                     continue;
27                 }
28             }
29             if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
30                 continue;
31             }
32             if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
33                 logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() + " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost()
34                         + ", supported protocol: " + ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
35                 continue;
36             }
37             URL url = mergeUrl(providerUrl);
38 
39             String key = url.toFullString(); // The parameter urls are sorted
40             if (keys.contains(key)) { // Repeated url
41                 continue;
42             }
43             keys.add(key);
44             // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
45             Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
46             Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
47             if (invoker == null) { // Not in the cache, refer again
48                 try {
49                     boolean enabled = true;
50                     if (url.hasParameter(Constants.DISABLED_KEY)) {
51                         enabled = !url.getParameter(Constants.DISABLED_KEY, false);
52                     } else {
53                         enabled = url.getParameter(Constants.ENABLED_KEY, true);
54                     }
55                     if (enabled) {
56                         invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
57                     }
58                 } catch (Throwable t) {
59                     logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
60                 }
61                 if (invoker != null) { // Put new invoker in cache
62                     newUrlInvokerMap.put(key, invoker);
63                 }
64             } else {
65                 newUrlInvokerMap.put(key, invoker);
66             }
67         }
68         keys.clear();
69         return newUrlInvokerMap;
70     }

         根据URL中的method参数构造一个以method为key的invoker的map,这是为了分组合并多个service的调用结果功能做准备。

 1     /**
 2      * Transform the invokers list into a mapping relationship with a method
 3      *
 4      * @param invokersMap Invoker Map
 5      * @return Mapping relation between Invoker and method
 6      */
 7     private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
 8         Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>();
 9         // According to the methods classification declared by the provider URL, the methods is compatible with the registry to execute the filtered methods
10         List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>();
11         if (invokersMap != null && invokersMap.size() > 0) {
12             for (Invoker<T> invoker : invokersMap.values()) {
13                 String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY);
14                 if (parameter != null && parameter.length() > 0) {
15                     String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter);
16                     if (methods != null && methods.length > 0) {
17                         for (String method : methods) {
18                             if (method != null && method.length() > 0
19                                     && !Constants.ANY_VALUE.equals(method)) {
20                                 List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
21                                 if (methodInvokers == null) {
22                                     methodInvokers = new ArrayList<Invoker<T>>();
23                                     newMethodInvokerMap.put(method, methodInvokers);
24                                 }
25                                 methodInvokers.add(invoker);
26                             }
27                         }
28                     }
29                 }
30                 invokersList.add(invoker);
31             }
32         }
33         List<Invoker<T>> newInvokersList = route(invokersList, null);
34         newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList);
35         if (serviceMethods != null && serviceMethods.length > 0) {
36             for (String method : serviceMethods) {
37                 List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
38                 if (methodInvokers == null || methodInvokers.size() == 0) {
39                     methodInvokers = newInvokersList;
40                 }
41                 newMethodInvokerMap.put(method, route(methodInvokers, method));
42             }
43         }
44         // sort and unmodifiable
45         for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) {
46             List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
47             Collections.sort(methodInvokers, InvokerComparator.getComparator());
48             newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers));
49         }
50         return Collections.unmodifiableMap(newMethodInvokerMap);
51     }

        以上逻辑实现了Directory接口,构造了注册中心目录服务的功能。RegistoryDirectory类本身还实现了NotifyListener接口,实现了notify()方法。在注册中心发布订阅模式中,RegistoryDirectory可作为订阅者订阅一个事件,如提供者、路由策略、配置规则的变动事件,事件发生后接受通知,并触发notify(urls)方法,接受新的提供者、路由策略、配置规则的url,并刷新本地url缓存。

 1     public synchronized void notify(List<URL> urls) {
 2         List<URL> invokerUrls = new ArrayList<URL>();
 3         List<URL> routerUrls = new ArrayList<URL>();
 4         List<URL> configuratorUrls = new ArrayList<URL>();
 5         for (URL url : urls) {
 6             String protocol = url.getProtocol();
 7             String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
 8             if (Constants.ROUTERS_CATEGORY.equals(category)
 9                     || Constants.ROUTE_PROTOCOL.equals(protocol)) {
10                 routerUrls.add(url);
11             } else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
12                     || Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
13                 configuratorUrls.add(url);
14             } else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
15                 invokerUrls.add(url);
16             } else {
17                 logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
18             }
19         }
20         // 刷新配置规则configurators
21         if (configuratorUrls != null && configuratorUrls.size() > 0) {
22             this.configurators = toConfigurators(configuratorUrls);
23         }
24         // 刷新路由规则routers
25         if (routerUrls != null && routerUrls.size() > 0) {
26             List<Router> routers = toRouters(routerUrls);
27             if (routers != null) { // null - do nothing
28                 setRouters(routers);
29             }
30         }
31         List<Configurator> localConfigurators = this.configurators; // local reference
32         // 合并以覆盖方式更新的参数merge override parameters
33         this.overrideDirectoryUrl = directoryUrl;
34         if (localConfigurators != null && localConfigurators.size() > 0) {
35             for (Configurator configurator : localConfigurators) {
36                 this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
37             }
38         }
39         // 刷新提供者invokers providers
40         refreshInvoker(invokerUrls);
41     }

        

        下面我们再讨论一下RegistoryProtocol类在注册中心所起的作用。如下图所示,RegistoryProtocol类是消费端和提供者端共用的类,蓝色虚线表示类初始化过程,即初始化时的组装链。

        首先看看export()实现逻辑:

        (1)调用doLocalExport(originInvoker)方法得到本地缓存的exporter(如果缓存找不到则用protocol.export()创建并放入缓存);

        (2)调用getRegistry(originInvoker)方法得到当前originInvoker的注册中心实例registry

 1     public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
 2         //从缓存中取得export,或者通过protocol.export()方法创建exporter export invoker
 3         final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
 4 
 5         URL registryUrl = getRegistryUrl(originInvoker);
 6 
 7         //registry provider
 8         final Registry registry = getRegistry(originInvoker);
 9         final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
10 
11         //取得是否立即注册的配置,默认为true to judge to delay publish whether or not
12         boolean register = registedProviderUrl.getParameter("register", true);
13         //将originInvoker加入本地缓存
14         ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);
15         //立即注册
16         if (register) {
//根据registryUrl取得相应的registry实例,调用registry.register()方法注册提供者url
17 register(registryUrl, registedProviderUrl); 18 ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true); 19 } 20 21 //订阅配置策略更新事件 Subscribe the override data 22 // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover. 23 final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl); 24 final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); 25 overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); 26 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); 27 //Ensure that a new exporter instance is returned every time export 28 return new Exporter<T>() { 29 public Invoker<T> getInvoker() { 30 return exporter.getInvoker(); 31 } 32 33 public void unexport() { 34 try { 35 exporter.unexport(); 36 } catch (Throwable t) { 37 logger.warn(t.getMessage(), t); 38 } 39 try { 40 registry.unregister(registedProviderUrl); 41 } catch (Throwable t) { 42 logger.warn(t.getMessage(), t); 43 } 44 try { 45 overrideListeners.remove(overrideSubscribeUrl); 46 registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener); 47 } catch (Throwable t) { 48 logger.warn(t.getMessage(), t); 49 } 50 } 51 }; 52 }

         接下来我们分析一下refer()方法的实现逻辑。refer()方法是客户端指向一个服务接口,用来获取一个服务接口的invoker(调用对象)远程请求服务方法的,顾名思义,RegisterProtocol协议类当然是从注册中心订阅获取一个invoker对象,这就涉及到对注册中心的操作。

实现逻辑大概是:

(1)通过URL得到相应的注册中心对象Registry(通常为zookeeper注册中心);

(2)如果请求的服务类别是RegistryService注册服务(猜测可能是管理控制台如dubbo-admin或监控台monitor用到的),则直接返回注册中心代理对象;

(3)请求的服务类别是普通服务接口,判断url中的group属性,如果是group="a,b" 或 group="*"的配置,则考虑分组聚合结果;

(4)调用doRefer()实现指向逻辑;

 1 public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
 2         url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
//通过URL得到相应的注册中心对象Registry(通常为zookeeper注册中心)
3 Registry registry = registryFactory.getRegistry(url);
//如果请求的服务类别是RegistryService注册服务(猜测可能是管理控制台如dubbo-admin或监控台monitor用到的),则直接返回注册中心代理对象
4 if (RegistryService.class.equals(type)) { 5 return proxyFactory.getInvoker((T) registry, type, url); 6 } 7 8 // group="a,b" or group="*" 请求的服务类别是普通服务接口,判断url中的group属性,如果是group="a,b" 或 group="*"的配置,则考虑分组聚合结果 9 Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY)); 10 String group = qs.get(Constants.GROUP_KEY); 11 if (group != null && group.length() > 0) { 12 if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1 13 || "*".equals(group)) { 14 return doRefer(getMergeableCluster(), registry, type, url); 15 } 16 } 17 return doRefer(cluster, registry, type, url); 18 }

doRefer()方法实现指向普通远程服务的逻辑:

(1)创建注册目录服务对象,并设置注册中心对象和协议;

(2)向注册中心注册客户端的服务指向事件,为管理控制台和监控台提供指向信息;

(3)从注册中心订阅所指向的服务接口提供列表;

(4)根据指定的集群策略,得到一个集群服务调用代理,该代理可根据指定的集群策略调用远程服务接口,该接口是从注册中心获得的可用服务(所指定的某个服务接口)列表,具体原理参见集群逻辑;

(5)将Consumer包装类对象存入本地缓存;

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
//创建注册目录服务对象,并设置注册中心对象和协议 RegistryDirectory
<T> directory = new RegistryDirectory<T>(type, url); directory.setRegistry(registry); directory.setProtocol(protocol); // all attributes of REFER_KEY Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters()); URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters); if (!Constants.ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(Constants.REGISTER_KEY, true)) {
//向注册中心注册客户端的服务指向事件,为管理控制台和监控台提供指向信息 registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY, Constants.CHECK_KEY, String.valueOf(
false))); }
//从注册中心订阅所指向的服务接口提供列表 directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY)); //根据指定的集群策略,得到一个集群服务调用代理,该代理可根据指定的集群策略调用远程服务接口,该接口是从注册中心获得的可用服务(所指定的某个服务接口)列表,具体原理参见集群逻辑 Invoker invoker = cluster.join(directory);
//将Consumer包装类对象存入本地缓存 ProviderConsumerRegTable.registerConsuemr(invoker, url, subscribeUrl, directory);
return invoker; }

        在介绍Zookeeper注册中心之前,我们讨论一下ZookeeperRegister的爷爷类AbstractRegistry类和父类FailbackRegistry类的实现原理(顾名思义,它实现了失败自动重试的注册服务。它本身继承了AbstractRegistry类)。

AbstractRegistry类实现了Registry接口,将注册、订阅的url写入HashMap结构的内存缓存,并在订阅接受通知时将订阅的url信息写入本地文件(用户文件夹.dubbo内)作为本地缓存。

        AbstractRegistry类的构造函数将本地文件url注册信息缓存载入内存,并触发通知notify()将新的url的集群备用地址写入文件缓存和notifys内存缓存。

 1     public AbstractRegistry(URL url) {
 2         setUrl(url);
 3         // Start file save timer
 4         syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
 5         String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
 6         File file = null;
 7         if (ConfigUtils.isNotEmpty(filename)) {
 8             file = new File(filename);
 9             if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
10                 if (!file.getParentFile().mkdirs()) {
11                     throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
12                 }
13             }
14         }
15         this.file = file;
//将本地文件url缓存载入内存
16 loadProperties();
//将url的集群备用地址写入本地文件和notifys内存缓存
17 notify(url.getBackupUrls()); 18 }

         以下是将本地properties内存缓存刷新到文件的代码,为了解决同步写入问题,增加了一个.lock文件作为锁。同时通过版本号乐观锁控制读取到最新的文件内容后再追加新的内容。

 1     public void doSaveProperties(long version) {
 2         if (version < lastCacheChanged.get()) {
 3             return;
 4         }
 5         if (file == null) {
 6             return;
 7         }
 8         // Save
 9         try {
//创建加锁文件
10 File lockfile = new File(file.getAbsolutePath() + ".lock"); 11 if (!lockfile.exists()) { 12 lockfile.createNewFile(); 13 } 14 RandomAccessFile raf = new RandomAccessFile(lockfile, "rw"); 15 try { 16 FileChannel channel = raf.getChannel(); 17 try { 18 FileLock lock = channel.tryLock(); 19 if (lock == null) { 20 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"); 21 } 22 // Save 23 try { 24 if (!file.exists()) { 25 file.createNewFile(); 26 } 27 FileOutputStream outputFile = new FileOutputStream(file); 28 try {
//写入内容
29 properties.store(outputFile, "Dubbo Registry Cache"); 30 } finally { 31 outputFile.close(); 32 } 33 } finally { 34 lock.release(); 35 } 36 } finally { 37 channel.close(); 38 } 39 } finally { 40 raf.close(); 41 } 42 } catch (Throwable e) { 43 if (version < lastCacheChanged.get()) { 44 return; 45 } else { 46 registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet())); 47 } 48 logger.warn("Failed to save registry store file, cause: " + e.getMessage(), e); 49 } 50 }

         register(url)方法将url写入registered内存缓存,subscribe(URL url, NotifyListener listener)方法将url和listener键值对写入listeners内存缓存,在zookeeperRegistry类的doSubscribe中触发notify,触发listener的notify方法。notify(urls)将urls列表写入notified内存缓存中,并触发对应url的listener.notify()方法。lookup(url)方法从notified本地内存缓存找到对应的url地址列表,如果不存在,则触发subscribe()方法订阅url。

         FailbackRegistry类是失败自动尝试恢复的注册类,继承了AbstractRegistry类。以下是构造方法,初始化了一个定时任务管理器,定时扫描注册/取消注册、订阅/取消订阅、通知的失败列表,重试注册/取消注册、订阅/取消订阅、通知任务,成功后从失败列表移除。在注册/取消注册、订阅/取消订阅、通知方法抛出异常时,自动将对应的url加入到失败列表等待重试。

 1     public FailbackRegistry(URL url) {
 2         super(url);
 3         int retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
 4         this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
 5             public void run() {
 6                 // Check and connect to the registry
 7                 try {
 8                     retry();
 9                 } catch (Throwable t) { // Defensive fault tolerance
10                     logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
11                 }
12             }
13         }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
14     }

         ZookeeperRegistry类基于Zookeeper目录服务,将注册的url信息写入zk,从zk中订阅url,从zk查找url对应的服务信息。

        该类主要实现了doRegister()、doUnRegister()、doSubscribe()、doUnSubscribe()、lookup()方法。

1     protected void doRegister(URL url) {
2         try {
//将url信息存入zk
3 zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true)); 4 } catch (Throwable e) { 5 throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 6 } 7 }
1     protected void doUnregister(URL url) {
2         try {
//从zk删除url信息
3 zkClient.delete(toUrlPath(url)); 4 } catch (Throwable e) { 5 throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 6 } 7 }

         doSubscribe()方法将订阅的url存入zk,触发notify()方法,调用listener.notify方法。      

 1     protected void doSubscribe(final URL url, final NotifyListener listener) {
 2         try {
 3             if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
 4                 String root = toRootPath();
 5                 ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
 6                 if (listeners == null) {
 7                     zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
 8                     listeners = zkListeners.get(url);
 9                 }
10                 ChildListener zkListener = listeners.get(listener);
11                 if (zkListener == null) {
12                     listeners.putIfAbsent(listener, new ChildListener() {
13                         public void childChanged(String parentPath, List<String> currentChilds) {
14                             for (String child : currentChilds) {
15                                 child = URL.decode(child);
16                                 if (!anyServices.contains(child)) {
17                                     anyServices.add(child);
18                                     subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
19                                             Constants.CHECK_KEY, String.valueOf(false)), listener);
20                                 }
21                             }
22                         }
23                     });
24                     zkListener = listeners.get(listener);
25                 }
26                 zkClient.create(root, false);
27                 List<String> services = zkClient.addChildListener(root, zkListener);
28                 if (services != null && services.size() > 0) {
29                     for (String service : services) {
30                         service = URL.decode(service);
31                         anyServices.add(service);
32                         subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
33                                 Constants.CHECK_KEY, String.valueOf(false)), listener);
34                     }
35                 }
36             } else {
37                 List<URL> urls = new ArrayList<URL>();
38                 for (String path : toCategoriesPath(url)) {
39                     ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
40                     if (listeners == null) {
41                         zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
42                         listeners = zkListeners.get(url);
43                     }
44                     ChildListener zkListener = listeners.get(listener);
45                     if (zkListener == null) {
46                         listeners.putIfAbsent(listener, new ChildListener() {
47                             public void childChanged(String parentPath, List<String> currentChilds) {
48                                 ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
49                             }
50                         });
51                         zkListener = listeners.get(listener);
52                     }
//path信息加入zk
53 zkClient.create(path, false); 54 List<String> children = zkClient.addChildListener(path, zkListener); 55 if (children != null) { 56 urls.addAll(toUrlsWithEmpty(url, path, children)); 57 } 58 } 59 notify(url, listener, urls); 60 } 61 } catch (Throwable e) { 62 throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 63 } 64 }

         lookup(url)方法从zk中查询所有相关的url服务列表:

 1     public List<URL> lookup(URL url) {
 2         if (url == null) {
 3             throw new IllegalArgumentException("lookup url == null");
 4         }
 5         try {
 6             List<String> providers = new ArrayList<String>();
 7             for (String path : toCategoriesPath(url)) {
//查询所有相关的url列表
8 List<String> children = zkClient.getChildren(path); 9 if (children != null) { 10 providers.addAll(children); 11 } 12 } 13 return toUrlsWithoutEmpty(url, providers); 14 } catch (Throwable e) { 15 throw new RpcException("Failed to lookup " + url + " from zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 16 } 17 }

         Dubbo出了实现基于zk的注册中心,还实现了基于redis的注册中心,multicast基于网络广播的注册中心,基本原理类似,在此不再详述。

        

猜你喜欢

转载自www.cnblogs.com/markcd/p/8525666.html
今日推荐