dubbo使用 org.apache.dubbo:dubbo:2.7.6及以后的版本,注册中心使用nacos
生产者端先启动注册到注册中心后,然后再启动消费者端,消费者提示no provider,经过检查,消费者,注册中心,生产者之间的网络没问题,消费者服务也能正常注册到。注册中心。此时已经束手无测了,只能深入源码探究了。
这里先说结论,在2.7.6版本后,如果服务url中未配置group,version字段,注册中心使用nacos。会导致在生产者先启动的情况下,再启动消费者再可能导致 no Provider的错误
在消费者订阅生产者服务时会根据消费者配置的生产者接口信息,生成生产中的服务名称(dubbo服务的唯一标识)。然后向注册中心进行订阅
那么直接来看关键方法
org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener)
@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
Set<String> serviceNames = getServiceNames(url, listener);
//Set corresponding serviceNames for easy search later
if (isServiceNamesWithCompatibleMode(url)) {
for (String serviceName : serviceNames) {
NacosInstanceManageUtil.setCorrespondingServiceNames(serviceName, serviceNames);
}
}
doSubscribe(url, listener, serviceNames);
}
获取服务名称之后进行订阅
再看服务名称的创建
org.apache.dubbo.registry.nacos.NacosRegistry#getServiceNames0
private Set<String> getServiceNames0(URL url) {
NacosServiceName serviceName = createServiceName(url);
final Set<String> serviceNames;
if (serviceName.isConcrete()) { // is the concrete service name
serviceNames = new LinkedHashSet<>();
serviceNames.add(serviceName.toString());
// Add the legacy service name since 2.7.6
String legacySubscribedServiceName = getLegacySubscribedServiceName(url);
if (!serviceName.toString().equals(legacySubscribedServiceName)) {
//avoid duplicated service names
serviceNames.add(legacySubscribedServiceName);
}
} else {
serviceNames = filterServiceNames(serviceName);
}
return serviceNames;
}
继续看创建过程
public NacosServiceName(URL url) {
serviceInterface = url.getParameter(INTERFACE_KEY);
category = isConcrete(serviceInterface) ? DEFAULT_CATEGORY : url.getParameter(CATEGORY_KEY);
version = url.getParameter(VERSION_KEY, DEFAULT_PARAM_VALUE);
group = url.getParameter(GROUP_KEY, DEFAULT_PARAM_VALUE);
value = toValue();
}
private String toValue() {
return category +
NAME_SEPARATOR + serviceInterface +
NAME_SEPARATOR + version +
NAME_SEPARATOR + group;
}
public static final String NAME_SEPARATOR = ":";
可以看出来dubbo服务的唯一标识名称 由 NAME_SEPARATOR(其实就是冒号:)作为间隔,最终是这个样子
category:serviceInterface:version:group的形式构成,并且如果version,group等可能为空的字段为空。冒号也不会省略
回到getServiceNames0中有一段为了兼容2.7.3版本之前的代码,因为在2.7.3之前如果没有category,需要给一个默认值。
// Add the legacy service name since 2.7.6
String legacySubscribedServiceName = getLegacySubscribedServiceName(url);
/**
* Get the legacy subscribed service name for compatible with Dubbo 2.7.3 and below
*
* @param url {@link URL}
* @return non-null
* @since 2.7.6
*/
private String getLegacySubscribedServiceName(URL url) {
StringBuilder serviceNameBuilder = new StringBuilder(DEFAULT_CATEGORY);
appendIfPresent(serviceNameBuilder, url, INTERFACE_KEY);
appendIfPresent(serviceNameBuilder, url, VERSION_KEY);
appendIfPresent(serviceNameBuilder, url, GROUP_KEY);
return serviceNameBuilder.toString();
}
private void appendIfPresent(StringBuilder target, URL url, String parameterName) {
String parameterValue = url.getParameter(parameterName);
if (!StringUtils.isBlank(parameterValue)) {
target.append(SERVICE_NAME_SEPARATOR).append(parameterValue);
}
}
其实和新版的类似,不过在appendIfPresent中可以看出来如果 version,group为空的话,间隔符冒号也会被省略
那么问题来了
if (!serviceName.toString().equals(legacySubscribedServiceName)) {
//avoid duplicated service names
serviceNames.add(legacySubscribedServiceName);
}
即使新版的 category未在url中指定,使用默认的provider,由于version,group为空。也会被认为,serviceName和legacySubscribedServiceName不相等
因为新版生成服务名称,version,group为空,不会忽略冒号,而legacySubscribedServiceName会忽略。
而此时我们的生产者也是2.7.6之后的版本注册到注册中心的服务也是不忽略冒号的形如
com.xishan.store.usercenter.userapi.facade.XXReadFacade::
而按照legacySubscribedServiceName订阅的是没有冒号的路径
com.xishan.store.usercenter.userapi.facade.XXReadFacade
而注册中心实际上是没有这个服务的。
在项目启动时会先后根据添加的者两个服务名称进行从nacos服务器上拉取实例信息。
看一个关键方法
org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker
private void refreshInvoker(List<URL> invokerUrls) {
Assert.notNull(invokerUrls, "invokerUrls should not be null");
if (invokerUrls.size() == 1
&& invokerUrls.get(0) != null
&& EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden = true; // Forbid to access
this.invokers = Collections.emptyList();
routerChain.setInvokers(this.invokers);
destroyAllInvokers(); // Close all invokers
} else {
this.forbidden = false; // Allow to access
····省略代码····
}
// notify invokers refreshed
this.invokersChanged();
}
可以看出来对于同一个RegistryDirectory,如果使用旧版服务名com.xishan.store.usercenter.userapi.facade.XXReadFacade晚于新版服务名com.xishan.store.usercenter.userapi.facade.XXReadFacade::来到refreshInvoker,会使invoker被清空,forbidden设置为true,当我们要使用dubbo服务时,报出no Provider
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
// 1. No service provider 2. Service providers are disabled
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry " +
getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +
NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() +
", please check status of providers(disabled, not registered or in blacklist).");
}
}