先复习下架构篇,对概念,类结构以及交互过程有个直观性认识。
《dubbo cluster–架构》
上图是一个订阅发布过程的概念模型,3个步骤并不是严格串行的,其实3个过程是异步解耦的,只是协作图上必须要有顺序。
客户端启动时会向Registry订阅服务事件,服务端启动时会向Registry注册服务,相应的Registry在注册完成后会通知客户端,客户端会在本地做provider(invoker)映射。
客户端Reference通过Protocol对象引用Invoker。
《dubbo spi》
//ReferenceConfig#loadRegistry方法。这个方法用来获取注册中心相关信息
//并对注册中心协议做隐藏处理,这里注意下,会面会解释
url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
//ReferenceConfig#createProxy方法。该方法生成一个暴露给消费者的远程服务引用(dubbo proxy),proxy里持有invoker对象,客户端与服务端的远程交互细节就是通过invoker来完成。
invoker = refprotocol.refer(interfaceClass, url);
loadRegistry方法将注册中心url的实际协议隐藏,暴露协议值为‘registry’,所以扩展点基于动态规则将会适配RegistryProtocol。
以下抽取RegistryProtocol#refer的关键逻辑,使用“图步骤x”映射架构篇里协作图上的具体步骤
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { //暴露真正的注册协议,以动态匹配具体的注册中心实现 url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY); //架构篇图步骤1,后面所有的图都是指架构篇协作图 Registry registry = registryFactory.getRegistry(url); // group="a,b" or group="*" if (//reference指定多个组别) { return doRefer( getMergeableCluster(), registry, type, url ); }
return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) { //图步骤2 RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url); //protocol是RegistryProtocol的实例变量,也是通过扩展点注入进来,其实就是Adaptive directory.setRegistry(registry); directory.setProtocol(protocol); URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters()); //图步骤3 directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY + "," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY)); //图步骤6 return cluster.join(directory); }
RegistryFactory也是个spi服务,依据具体的注册中心协议初始化一个Registry,并且set给RegistryDirectory。最后通过RegistryDirectory#subscribe订阅事件推送。
public void subscribe(URL url) {
this.subscribeUrl = url;
registry.subscribe(url, this);
}
Registry#subscribe方法,先看下接口定义
void subscribe(URL url, NotifyListener listener);
该方法第二个参数是一个NotifyListener,RegistryDirectory继承了它,在RegistryDirectory#subscribe中将this对象就直接传递进去,由RegistryDirectory接受事件推送。
以下以zookeeper注册中心举例说明Registry#subscribe。
Zookeeper对每个provider的每类主题都单独写一个文件,路径名:group/interface name/category/url(protocol://username:password@host/path),category后的url为每个provider的具体地址信息,由provider上报时提供。
每个consumer会watch自己使用服务的对应目录的childChnaged时间,一般是providers,configurators和routers。目录下有写入或者删除事件时会通知监听者,也就是RegistryDirectory,通知事件中包含当前监听目录下所有的path信息,这些path信息由provider在初始化时写入。
protected void doSubscribe(final URL url, final NotifyListener listener) {
//省略了订阅所有provider事件部分逻辑
List<URL> urls = new ArrayList<URL>();
for (String path : toCategoriesPath(url)) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
//省略部分非主要逻辑
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
public void childChanged(String parentPath, List<String> currentChilds) {
ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
zkListener = listeners.get(listener);
}
//zookeeper下创建目录
zkClient.create(path, true);
//图步骤4
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
notify(url, listener, urls);
}
private String[] toCategoriesPath(URL url) {
String[] categroies;
//省略了订阅所有事件部分逻辑
else {
categroies = url.getParameter(Constants.CATEGORY_KEY, new String[] {Constants.DEFAULT_CATEGORY});
}
String[] paths = new String[categroies.length];
for (int i = 0; i < categroies.length; i ++) {
paths[i] = toServicePath(url) + Constants.PATH_SEPARATOR + categroies[i];
}
return paths;
}
一个directory可以同时订阅多个主题,toCategoriesPath会为每个主题(category)都生成一个路径。客户端会在zookeeper下写入目录,并订阅目录下的写入及变更事件。
最后看一下RegistryDirectory#notify方法,Zookeeper推送的事件最终会由这个方法接受。
public synchronized void notify(List<URL> urls) {
//精简了router和configurator逻辑
List<URL> invokerUrls = new ArrayList<URL>();
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
}
}
//刷新客户端的provider(invoker)数据
refreshInvoker(invokerUrls);
}
refreshInvoker方法会刷新客户端的invoker数据,移除失效的,加入新增的。
RegistryDirectory里有两种invoker map,一种是urlInvokerMap,另一种是methodInvokerMap。前者主要是用来每次推送时和最新invoker列表做对比,清除下线的invoker信息。
另一个,methodInvokerMap,它里面保存的数据最终会作为directory对外暴露的invoker。methodInvokerMap是将invoker按照method分组,可以通过methods配置项声明provider指定给哪个方法使用,没有指定methods的provider则对这个接口的所有消费者可见。