Dubbo中@DubboReference.version如何设置为*,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
Dubbo在消费端提供了一个功能,即将消费者的版本号指定为*,那么不管服务端的接口版本是啥,都可以调用成功。
1 初步猜测
dubbo接口定位逻辑:接口(全路径)+服务分组(group字段)+版本号(version字段)。
Zookeeper 是用树状来保存数据的,在 Zookeeper 中,可以利用Dubbo接口(全路径)作为父节点,再根据group和version信息写入子节点。
而 Nacos,在 Nacos 的控制台中,我们看到可以根据服务名或服务分组来模糊查询服务列表,那么在消费者订阅的时候,就根据这两个模糊查询就可以了,查出来的健康提供者都是符合的。
下面就深入一下源码,看看实际的逻辑是不是类似我们的猜想。
2 源码剖析
2.1 Zookeeper 作为注册中心
2.1.1 准备
弄一个服务提供者、一个服务消费者。服务提供者对外提供一个dubbo接口,版本有1.0.0和2.0.0;服务消费者引入服务提供者提供的dubbo接口,version设置为*。
启动服务提供者、接着启动消费者,观察后台日志打印:
我们可以看到,当我们将@DubboReference的version设置为*的时候,他就根据注册url(带*)去找有哪些服务提供者,然后返回的urls会有多个,其中包含版本号为1.0.0和2.0.0的url。
2021-05-01 10:24:08.561 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Subscribe: consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, dubbo version: 2.7.7, current host: 127.0.0.1 2021-05-01 10:24:08.572 [main] [INFO ] [o.a.d.r.z.ZookeeperRegistry] [] [] - [DUBBO] Notify urls for subscribe url consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, urls: [dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=1.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820516&version=1.0.0, dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=2.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820187&version=2.0.0, empty://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=configurators&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*, empty://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=14021&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619835848549&version=*], dubbo version: 2.7.7, current host: 127.0.0.1
2.1.2 源码分析
上面我们看到,version=*可以成功订阅,并且服务提供者有两个,分别是version=1.0.0和version=2.0.0。
结果是看得出来了,但是我们还是需要看看Zookeeper是怎么的判断逻辑。
2.1.2.1 服务消费者订阅过程
我们都知道,正常发布Dubbo的消费者,需要配置ReferenceConfig,然后调用export方法;当然了,我们这里就不过于深入了,直接从日志的入口来开始:org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
我们服务消费者的订阅url:
consumer://127.0.0.1/com.winfun.service.DubboServiceOne?application=dubbo-service&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&methods=sayHello&pid=16215&qos.enable=false&reference.filter=default,dubboLogFilter,sentinel.dubbo.consumer.filter&release=2.7.7&revision=*&side=consumer&sticky=false×tamp=1619842106726&version=*
第一步:获取dubbo接口全路径
url.getServiceInterface() -> com.winfun.service.DubboServiceOne
接着判断是否等于“*”,明显不是,跳到else分支
第二步:根据url获取path
获取根节点:
toCategoriesPath(url) -> /dubbo/com.winfun.service.DubboServiceOne/providers、/dubbo/com.winfun.service.DubboServiceOne/configurators、/dubbo/com.winfun.service.DubboServiceOne/consumers
第三步:遍历第二步的path、创建父节点
重点在path=/dubbo/com.winfun.service.DubboServiceOne/providers,其他忽略即可
根据path创建节点(非持久化):zkClient.create(root, false);
给path添加子节点监听器:zkClient.addChildListener(path, zkListener) 并返回子节点列表
dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=1.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820516&version=1.0.0
dubbo://192.168.2.10:20880/com.winfun.service.DubboServiceOne?anyhost=true&application=dubbo-provider-one&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=13996&release=2.7.8&revision=2.0.0&service.filter=default,dubboLogFilter&side=provider×tamp=1619835820187&version=2.0.0
configurators 和 consumers 不存在子节点,所以子节点是根据规则生成的url,前缀为empty
第四步:对上面获取到的urls进行监听
调用org.apache.dubbo.registry.support.FailbackRegistry#notify方法。
最后会去到 org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List
重点:在监听前,会先匹配根据path查询的所有子节点中,匹配符合当前消费者的子节点(根据group和version判断),利用org.apache.dubbo.common.utils.UrlUtils#isMatch判断。
判断中最重要的逻辑:
String ANY_VALUE = "*"; String consumerGroup = consumerUrl.getParameter(GROUP_KEY); String consumerVersion = consumerUrl.getParameter(VERSION_KEY); String consumerClassifier = consumerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE); String providerGroup = providerUrl.getParameter(GROUP_KEY); String providerVersion = providerUrl.getParameter(VERSION_KEY); String providerClassifier = providerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE); return (ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup)) && (ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion)) && (consumerClassifier == null || ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier));
上面完全可以体现出:当版本号等于*号,dubbo接口根节点下的服务都会作为当前消费者的服务提供者。
好了,到这里,我们可以知道Zookeeper是怎么为version=*的消费者订阅服务的,直接根据接口全路径名到Zookeeper里获取所有子节点,并都可以作为服务提供者。
其实这里会有一个扩展点:多个服务提供者,调用的时候是怎么负载的,其实在@DubboReference中的loadbance属性中看得出,默认的负载策略是随机。
String loadbalance() default ""; org.apache.dubbo.common.constants.CommonConstants#DEFAULT_LOADBALANCE="random";
2.1.2.2 服务消费者执行过程
proxy执行入口:
我们可以通过debug模式,进入到Dubbo方法执行的入口:org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke
第一步:初步判断
如果是 Object类 或者 toString、destory、hashCode等方法,直接执行
第二步:创建RpcInvocation
根据执行方法、参数等信息创建RpcInvocation
获取serviceKey:-> serviceKey = dubbo-api-path/group:version
RpcInvocation设置TargetServiceUniqueName
第三步:调用invoker的invoke方法
来到org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#invoke
判断是否设置了 mock 或 force
如果是调用org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#doMockInvoke方法
否则来到org.apache.dubbo.rpc.cluster.support.wrapper.AbstractCluster.InterceptorInvokerNode#invoke
第四步:AbstractClusterInvoker#invoke
调用AbstractClusterInvoker#list获取invoker列表,可以看到拿到的就是1.0.0版本和2.0.0版本的服务提供者
接着调用 initLoadBalance 方法来初始化负载均衡策略,从订阅url里面获取loadbalance的值,如果没有设置,返回默认值“random”
protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) { if (CollectionUtils.isNotEmpty(invokers)) { return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl() .getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE)); } else { return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE); } }
第五步:根据集群策略执行方法
由于Dubbo默认的集群策略是 failover,所以会来到来到:org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoker
首先会从注册url里面的retries字段获取重试次数(如果为空,默认重试次数为2),此次取的是默认值,所以最后最大调用次数为3.
循环retries+1次
来到:org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#select 选择Invoker
在RandomLoadBalance#doSelect 中,首先会根据服务提供者的权重判断,如果权重没赋值,最后会利用ThreadLocalRandom.current().nextInt(invokers.size())随机选择一个invoker。
下一步:org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#doSelect
下一步:org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance#select由于默认是random的负载均衡策略,所以最后来到:org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance#doSelect
执行invoke方法,返回结果
如果有错误,记录着,下次循环打印warn日志
如果超过retries+1次调用失败,往外抛出RpcException异常
到这里,我们已经非常清楚Zookeeper 是如何支持消费者将 version设置为*,并且方法调用时是如何选择服务提供者。
2.2 Nacos 作为注册中心
2.2.1 准备
弄一个服务提供者、一个服务消费者,这次不再是Zookeeper作为注册中心,而是Nacos作为注册中心。服务提供者对外提供一个dubbo接口,版本有1.0.0和2.0.0;服务消费者引入服务提供者提供的dubbo接口,version设置为*。
启动服务提供者、接着启动消费者,观察后台日志打印:
2.2.2 源码分析
2.2.2.1 服务消费者订阅过程
Nacos 源码分析也是直接从 NacosRegistry#doSubscribe 入口开始:
org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener)
消费者的注册url:
consumer://192.168.3.3/com.winfun.service.DubboServiceOne?application=dubbo-consumer-nacos&category=providers,configurators,routers&dubbo=2.0.2&init=false&interface=com.winfun.service.DubboServiceOne&metadata-type=remote&methods=sayHello&pid=39203&qos.enable=false&reference.filter=default,dubboLogFilter&release=2.7.8&revision=*&side=consumer&sticky=false×tamp=1620177626113&version=*
第一步、根据url获取serviceName集合:
org.apache.dubbo.registry.nacos.NacosRegistry#getServiceNames0
1、创建 NacosServiceName:
providers:com.winfun.service.DubboServiceOne:*:
2、接着到:org.apache.dubbo.registry.nacos.NacosRegistry#filterServiceNames(org.apache.dubbo.registry.nacos.NacosServiceName)
根据上面的servicename过滤出所有的serviceName
2.1、 先利用NamingProxy查询:
com.alibaba.nacos.client.naming.net.NamingProxy#getServiceList(int, int, java.lang.String, com.alibaba.nacos.api.selector.AbstractSelector)
利用接口全路径名+group查询,没有带版本号
2.2、最后到:
com.alibaba.nacos.common.http.client.NacosRestTemplate#exchangeForm
http 请求:
url:
http://127.0.0.1:8848/nacos/v1/ns/service/list params:{app=unknown, pageSize=2147483647, groupName=DEFAULT_GROUP, namespaceId=public, pageNo=1}
返回结果:
RestResult{code=200, message='null', data={"doms":["providers:com.winfun.service.DubboServiceOne:1.0.0:","providers:com.winfun.service.DubboServiceOne:2.0.0:"],"count":2}}
明显包含两个版本的service
第二步、根据条件过滤合适的 service
public boolean isCompatible(NacosServiceName concreteServiceName) { if (!concreteServiceName.isConcrete()) { // The argument must be the concrete NacosServiceName return false; } // Not match comparison if (!StringUtils.isEquals(this.category, concreteServiceName.category) && !matchRange(this.category, concreteServiceName.category)) { return false; } if (!StringUtils.isEquals(this.serviceInterface, concreteServiceName.serviceInterface)) { return false; } // wildcard condition // 重点在这里 if (isWildcard(this.version)) { return true; } if (isWildcard(this.group)) { return true; } // range condition if (!StringUtils.isEquals(this.version, concreteServiceName.version) && !matchRange(this.version, concreteServiceName.version)) { return false; } if (!StringUtils.isEquals(this.group, concreteServiceName.group) && !matchRange(this.group, concreteServiceName.group)) { return false; } return true; } private boolean isWildcard(String value) { return WILDCARD.equals(value); } public static final String WILDCARD = "*";
过滤后的 service 有两个,分别是1.0.0和2.0.0
那么继续深一步的订阅流程:org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.Set
第三步、遍历serviceNames,根据serviceName+group查询所有实例列表并且进行实例监听
List<Instance> instances = new LinkedList<>(); for (String serviceName : serviceNames) { instances.addAll(namingService.getAllInstances(serviceName , getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP))); notifySubscriber(url, listener, instances); subscribeEventListener(serviceName, url, listener); }
到这里,整个订阅流程已经结束,主要是看version=*如何判断哪些服务实例可提供服务,再深入的就没有了。
Nacos 作为注册中心,查询服务实例主要是根据 serviceName(接口全路径名)和group(分组),这是因为Nacos的数据结构本身主要的就是服务名+分组名。
2.2.2.2 服务消费者调用过程
这个就不再深入讲解了,调用过程和 Zookeeper 上基本一致。
关于Dubbo中@DubboReference.version如何设置为*问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注编程网行业资讯频道了解更多相关知识。