You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2021/07/16 02:18:10 UTC

[dubbo] branch 3.0 updated: [3.0] Lock-free ConfigManager and improve config checking (#8289)

This is an automated email from the ASF dual-hosted git repository.

albumenj pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/dubbo.git


The following commit(s) were added to refs/heads/3.0 by this push:
     new a5737d1  [3.0] Lock-free ConfigManager and improve config checking (#8289)
a5737d1 is described below

commit a5737d1625259837cff4555b94ac92301eb56642
Author: Gong Dewei <ky...@qq.com>
AuthorDate: Fri Jul 16 10:17:58 2021 +0800

    [3.0] Lock-free ConfigManager and improve config checking (#8289)
    
    * Improve config equals() and toString() performance
    
    * Lock-free ConfigManager through ConcurrentHashMap, improve reference config checking
---
 .../org/apache/dubbo/config/AbstractConfig.java    | 143 +++++++--------
 .../dubbo/config/AbstractInterfaceConfig.java      |  15 +-
 .../apache/dubbo/config/context/ConfigManager.java | 197 ++++++++-------------
 .../apache/dubbo/config/ReferenceConfigTest.java   |  92 ++++++++--
 4 files changed, 230 insertions(+), 217 deletions(-)

diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
index 5b8c4e1..efe5941 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
@@ -67,9 +67,14 @@ public abstract class AbstractConfig implements Serializable {
     private static final long serialVersionUID = 4267533505537413570L;
 
     /**
-     * The field names cache of config class
+     * tag name cache, speed up get tag name frequently
      */
-    private static final Map<Class, Set<String>> fieldNamesCache = new ConcurrentHashMap<>();
+    private static final Map<Class, String> tagNameCache = new ConcurrentHashMap<>();
+
+    /**
+     * attributed getter method cache for equals(), hashCode() and toString()
+     */
+    private static final Map<Class, List<Method>> attributedMethodCache = new ConcurrentHashMap<>();
 
     /**
      * The suffix container
@@ -90,14 +95,16 @@ public abstract class AbstractConfig implements Serializable {
 
 
     public static String getTagName(Class<?> cls) {
-        String tag = cls.getSimpleName();
-        for (String suffix : SUFFIXES) {
-            if (tag.endsWith(suffix)) {
-                tag = tag.substring(0, tag.length() - suffix.length());
-                break;
+        return tagNameCache.computeIfAbsent(cls, (key)-> {
+            String tag = cls.getSimpleName();
+            for (String suffix : SUFFIXES) {
+                if (tag.endsWith(suffix)) {
+                    tag = tag.substring(0, tag.length() - suffix.length());
+                    break;
+                }
             }
-        }
-        return StringUtils.camelToSplitName(tag, "-");
+            return StringUtils.camelToSplitName(tag, "-");
+        });
     }
 
     public static String getPluralTagName(Class<?> cls) {
@@ -134,7 +141,7 @@ public abstract class AbstractConfig implements Serializable {
         if (config == null) {
             return;
         }
-        // If asParameters=false, it means as attributes, ignore @Parameter annotation except 'append' and 'attribute'
+        // If asParameters=false, it means append attributes, ignore @Parameter annotation's attributes except 'append' and 'attribute'
 
         // How to select the appropriate one from multiple getter methods of the property?
         // e.g. Using String getGeneric() or Boolean isGeneric()? Judge by field type ?
@@ -426,7 +433,7 @@ public abstract class AbstractConfig implements Serializable {
         return metaData;
     }
 
-    protected static BeanInfo getBeanInfo(Class cls) {
+    private static BeanInfo getBeanInfo(Class cls) {
         BeanInfo beanInfo = null;
         try {
             beanInfo = Introspector.getBeanInfo(cls);
@@ -641,29 +648,14 @@ public abstract class AbstractConfig implements Serializable {
     public String toString() {
         try {
 
-            Set<String> fieldNames = getFieldNames(this.getClass());
-
             StringBuilder buf = new StringBuilder();
             buf.append("<dubbo:");
             buf.append(getTagName(getClass()));
-            Method[] methods = getClass().getMethods();
-            for (Method method : methods) {
+            for (Method method : getAttributedMethods()) {
                 try {
                     if (MethodUtils.isGetter(method)) {
                         String name = method.getName();
                         String key = calculateAttributeFromGetter(name);
-
-                        // Fixes #4992, endless recursive call when NetUtils method fails.
-                        if (!fieldNames.contains(key)) {
-                            continue;
-                        }
-
-                        // filter non attribute
-                        Parameter parameter = method.getAnnotation(Parameter.class);
-                        if (parameter != null && !parameter.attribute()) {
-                            continue;
-                        }
-
                         Object value = method.invoke(this);
                         if (value != null) {
                             buf.append(' ');
@@ -685,10 +677,6 @@ public abstract class AbstractConfig implements Serializable {
         }
     }
 
-    private static Set<String> getFieldNames(Class<?> configClass) {
-        return fieldNamesCache.computeIfAbsent(configClass, ReflectUtils::getAllFieldNames);
-    }
-
     @Override
     public boolean equals(Object obj) {
         if (obj == null || obj.getClass() != this.getClass()) {
@@ -698,33 +686,19 @@ public abstract class AbstractConfig implements Serializable {
             return true;
         }
 
-        BeanInfo beanInfo = getBeanInfo(this.getClass());
-        for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
-            Method method = methodDescriptor.getMethod();
-            if (MethodUtils.isGetter(method)) {
-                // filter non attribute
-                Parameter parameter = method.getAnnotation(Parameter.class);
-                if (parameter != null && !parameter.attribute()) {
-                    continue;
-                }
-                String propertyName = calculateAttributeFromGetter(method.getName());
-                // ignore compare 'id' value
-                if (Constants.ID.equals(propertyName)) {
-                    continue;
-                }
-                // filter non writable property
-                if (!isWritableProperty(beanInfo, propertyName)) {
-                    continue;
-                }
-                try {
-                    Object value1 = method.invoke(this);
-                    Object value2 = method.invoke(obj);
-                    if (!Objects.equals(value1, value2)) {
-                        return false;
-                    }
-                } catch (Exception e) {
-                    throw new IllegalStateException("compare config instances failed", e);
+        for (Method method : getAttributedMethods()) {
+            // ignore compare 'id' value
+            if ("getId".equals(method.getName())) {
+                continue;
+            }
+            try {
+                Object value1 = method.invoke(this);
+                Object value2 = method.invoke(obj);
+                if (!Objects.equals(value1, value2)) {
+                    return false;
                 }
+            } catch (Exception e) {
+                throw new IllegalStateException("compare config instances failed", e);
             }
         }
         return true;
@@ -734,28 +708,57 @@ public abstract class AbstractConfig implements Serializable {
     public int hashCode() {
         int hashCode = 1;
 
-        Method[] methods = this.getClass().getMethods();
-        for (Method method : methods) {
-            if (MethodUtils.isGetter(method)) {
-                Parameter parameter = method.getAnnotation(Parameter.class);
-                // filter non attribute
-                if (parameter != null && !parameter.attribute()) {
-                    continue;
-                }
-                try {
-                    Object value = method.invoke(this);
+        for (Method method : getAttributedMethods()) {
+            // ignore compare 'id' value
+            if ("getId".equals(method.getName())) {
+                continue;
+            }
+            try {
+                Object value = method.invoke(this);
+                if (value != null) {
                     hashCode = 31 * hashCode + value.hashCode();
-                } catch (Exception ignored) {
-                    //ignored
                 }
+            } catch (Exception ignored) {
+                //ignored
             }
         }
 
         if (hashCode == 0) {
             hashCode = 1;
         }
-
         return hashCode;
     }
 
+    private List<Method> getAttributedMethods() {
+        Class<? extends AbstractConfig> cls = this.getClass();
+        return attributedMethodCache.computeIfAbsent(cls, (key)-> computeAttributedMethods());
+    }
+
+    /**
+     * compute attributed getter methods, subclass can override this method to add/remove attributed methods
+     * @return
+     */
+    protected List<Method> computeAttributedMethods() {
+        Class<? extends AbstractConfig> cls = this.getClass();
+        BeanInfo beanInfo = getBeanInfo(cls);
+        List<Method> methods = new ArrayList<>(beanInfo.getMethodDescriptors().length);
+        for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
+            Method method = methodDescriptor.getMethod();
+            if (MethodUtils.isGetter(method)) {
+                // filter non attribute
+                Parameter parameter = method.getAnnotation(Parameter.class);
+                if (parameter != null && !parameter.attribute()) {
+                    continue;
+                }
+                String propertyName = calculateAttributeFromGetter(method.getName());
+                // filter non writable property, exclude non property methods, fix #4225
+                if (!isWritableProperty(beanInfo, propertyName)) {
+                    continue;
+                }
+                methods.add(method);
+            }
+        }
+        return methods;
+    }
+
 }
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractInterfaceConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractInterfaceConfig.java
index bc4f177..c8dd6c1 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractInterfaceConfig.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractInterfaceConfig.java
@@ -25,7 +25,6 @@ import org.apache.dubbo.common.utils.CollectionUtils;
 import org.apache.dubbo.common.utils.ConfigUtils;
 import org.apache.dubbo.common.utils.ReflectUtils;
 import org.apache.dubbo.common.utils.StringUtils;
-import org.apache.dubbo.config.context.ConfigManager;
 import org.apache.dubbo.config.support.Parameter;
 import org.apache.dubbo.rpc.model.ApplicationModel;
 import org.apache.dubbo.rpc.model.ServiceMetadata;
@@ -661,12 +660,7 @@ public abstract class AbstractInterfaceConfig extends AbstractMethodConfig {
     public void setConfigCenter(ConfigCenterConfig configCenter) {
         this.configCenter = configCenter;
         if (configCenter != null) {
-            ConfigManager configManager = ApplicationModel.getConfigManager();
-            Collection<ConfigCenterConfig> configs = configManager.getConfigCenters();
-            if (CollectionUtils.isEmpty(configs)
-                    || configs.stream().noneMatch(existed -> existed.equals(configCenter))) {
-                configManager.addConfigCenter(configCenter);
-            }
+            ApplicationModel.getConfigManager().addConfigCenter(configCenter);
         }
     }
 
@@ -718,12 +712,7 @@ public abstract class AbstractInterfaceConfig extends AbstractMethodConfig {
     public void setMetadataReportConfig(MetadataReportConfig metadataReportConfig) {
         this.metadataReportConfig = metadataReportConfig;
         if (metadataReportConfig != null) {
-            ConfigManager configManager = ApplicationModel.getConfigManager();
-            Collection<MetadataReportConfig> configs = configManager.getMetadataConfigs();
-            if (CollectionUtils.isEmpty(configs)
-                    || configs.stream().noneMatch(existed -> existed.equals(metadataReportConfig))) {
-                configManager.addMetadataReport(metadataReportConfig);
-            }
+            ApplicationModel.getConfigManager().addMetadataReport(metadataReportConfig);
         }
     }
 
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/context/ConfigManager.java b/dubbo-common/src/main/java/org/apache/dubbo/config/context/ConfigManager.java
index 56d72a3..36d8ed4 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/config/context/ConfigManager.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/context/ConfigManager.java
@@ -48,19 +48,12 @@ import org.apache.dubbo.rpc.model.ApplicationModel;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.stream.Collectors;
 
 import static java.lang.Boolean.TRUE;
@@ -69,6 +62,10 @@ import static java.util.Optional.ofNullable;
 import static org.apache.dubbo.common.utils.StringUtils.isNotEmpty;
 import static org.apache.dubbo.config.AbstractConfig.getTagName;
 
+/**
+ * A lock-free config manager (through ConcurrentHashMap), for fast read operation.
+ * The Write operation lock with sub configs map of config type, for safely check and add new config.
+ */
 public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
 
     private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
@@ -78,15 +75,13 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     private static final String CONFIG_NAME_READ_METHOD = "getName";
     public static final String DUBBO_CONFIG_MODE = ConfigKeys.DUBBO_CONFIG_MODE;
 
-    private final ReadWriteLock lock = new ReentrantReadWriteLock();
-
-    final Map<String, Map<String, AbstractConfig>> configsCache = newMap();
+    final Map<String, Map<String, AbstractConfig>> configsCache = new ConcurrentHashMap<>();
 
-    private Map<String, AbstractInterfaceConfig> referenceConfigCache = new HashMap<>();
+    private Map<String, AbstractInterfaceConfig> referenceConfigCache = new ConcurrentHashMap<>();
 
-    private Map<String, AbstractInterfaceConfig> serviceConfigCache = new HashMap<>();
+    private Map<String, AbstractInterfaceConfig> serviceConfigCache = new ConcurrentHashMap<>();
 
-    private Set<AbstractConfig> duplicatedConfigs = new HashSet<>();
+    private Set<AbstractConfig> duplicatedConfigs = new ConcurrentHashSet<>();
 
     private ConfigMode configMode = ConfigMode.STRICT;
 
@@ -407,22 +402,19 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     }
 
     public void refreshAll() {
-        write(() -> {
-            // refresh all configs here,
-            getApplication().ifPresent(ApplicationConfig::refresh);
-            getMonitor().ifPresent(MonitorConfig::refresh);
-            getModule().ifPresent(ModuleConfig::refresh);
-            getMetrics().ifPresent(MetricsConfig::refresh);
-            getSsl().ifPresent(SslConfig::refresh);
-
-            getProtocols().forEach(ProtocolConfig::refresh);
-            getRegistries().forEach(RegistryConfig::refresh);
-            getProviders().forEach(ProviderConfig::refresh);
-            getConsumers().forEach(ConsumerConfig::refresh);
-            getConfigCenters().forEach(ConfigCenterConfig::refresh);
-            getMetadataConfigs().forEach(MetadataReportConfig::refresh);
-        });
+        // refresh all configs here,
+        getApplication().ifPresent(ApplicationConfig::refresh);
+        getMonitor().ifPresent(MonitorConfig::refresh);
+        getModule().ifPresent(ModuleConfig::refresh);
+        getMetrics().ifPresent(MetricsConfig::refresh);
+        getSsl().ifPresent(SslConfig::refresh);
 
+        getProtocols().forEach(ProtocolConfig::refresh);
+        getRegistries().forEach(RegistryConfig::refresh);
+        getProviders().forEach(ProviderConfig::refresh);
+        getConsumers().forEach(ConsumerConfig::refresh);
+        getConfigCenters().forEach(ConfigCenterConfig::refresh);
+        getMetadataConfigs().forEach(MetadataReportConfig::refresh);
     }
 
     /**
@@ -442,13 +434,11 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     }
 
     public void clear() {
-        write(() -> {
-            this.configsCache.clear();
-            configIdIndexes.clear();
-            this.referenceConfigCache.clear();
-            this.serviceConfigCache.clear();
-            this.duplicatedConfigs.clear();
-        });
+        this.configsCache.clear();
+        configIdIndexes.clear();
+        this.referenceConfigCache.clear();
+        this.serviceConfigCache.clear();
+        this.duplicatedConfigs.clear();
     }
 
     /**
@@ -484,10 +474,22 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
         if (config instanceof MethodConfig) {
             return null;
         }
-        return (T) write(() -> {
-            Map<String, AbstractConfig> configsMap = configsCache.computeIfAbsent(getTagName(config.getClass()), type -> newMap());
-            return addIfAbsent(config, configsMap, unique);
-        });
+
+        Map<String, AbstractConfig> configsMap = configsCache.computeIfAbsent(getTagName(config.getClass()), type -> newMap());
+
+        // fast check duplicated equivalent config before write lock
+        if (!(config instanceof ReferenceConfigBase || config instanceof ServiceConfigBase)) {
+            for (AbstractConfig value : configsMap.values()) {
+                if (value.equals(config)) {
+                    return (T) value;
+                }
+            }
+        }
+
+        // lock by config type
+        synchronized (configsMap) {
+            return (T) addIfAbsent(config, configsMap, unique);
+        }
     }
 
     public <C extends AbstractConfig> Map<String, C> getConfigsMap(Class<C> cls) {
@@ -495,15 +497,15 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     }
 
     private <C extends AbstractConfig> Map<String, C> getConfigsMap(String configType) {
-        return (Map<String, C>) read(() -> configsCache.getOrDefault(configType, emptyMap()));
+        return (Map<String, C>) configsCache.getOrDefault(configType, emptyMap());
     }
 
     private <C extends AbstractConfig> Collection<C> getConfigs(String configType) {
-        return (Collection<C>) read(() -> getConfigsMap(configType).values());
+        return (Collection<C>) getConfigsMap(configType).values();
     }
 
     public <C extends AbstractConfig> Collection<C> getConfigs(Class<C> configType) {
-        return (Collection<C>) read(() -> getConfigsMap(getTagName(configType)).values());
+        return (Collection<C>) getConfigsMap(getTagName(configType)).values();
     }
 
     /**
@@ -513,10 +515,7 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
      * @return
      */
     private <C extends AbstractConfig> C getConfigById(String configType, String id) {
-        return read(() -> {
-            Map<String, C> configsMap = (Map) configsCache.getOrDefault(configType, emptyMap());
-            return configsMap.get(id);
-        });
+        return (C) getConfigsMap(configType).get(id);
     }
 
     /**
@@ -526,26 +525,23 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
      * @return
      */
     private <C extends AbstractConfig> C getConfigByName(Class<? extends C> cls, String name) {
-        return read(() -> {
-            String configType = getTagName(cls);
-            Map<String, C> configsMap = (Map) configsCache.getOrDefault(configType, emptyMap());
-            if (configsMap.isEmpty()) {
-                return null;
-            }
-            // try find config by name
-            if (ReflectUtils.hasMethod(cls, CONFIG_NAME_READ_METHOD)) {
-                List<C> list = configsMap.values().stream()
-                        .filter(cfg -> name.equals(getConfigName(cfg)))
-                        .collect(Collectors.toList());
-                if (list.size() > 1) {
-                    throw new IllegalStateException("Found more than one config by name: " + name +
-                            ", instances: " + list + ". Please remove redundant configs or get config by id.");
-                } else if (list.size() == 1) {
-                    return list.get(0);
-                }
-            }
+        Map<String, ? extends C> configsMap = getConfigsMap(cls);
+        if (configsMap.isEmpty()) {
             return null;
-        });
+        }
+        // try find config by name
+        if (ReflectUtils.hasMethod(cls, CONFIG_NAME_READ_METHOD)) {
+            List<C> list = configsMap.values().stream()
+                .filter(cfg -> name.equals(getConfigName(cfg)))
+                .collect(Collectors.toList());
+            if (list.size() > 1) {
+                throw new IllegalStateException("Found more than one config by name: " + name +
+                    ", instances: " + list + ". Please remove redundant configs or get config by id.");
+            } else if (list.size() == 1) {
+                return list.get(0);
+            }
+        }
+        return null;
     }
 
     private <C extends AbstractConfig> String getConfigName(C config) {
@@ -557,56 +553,16 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     }
 
     protected <C extends AbstractConfig> C getSingleConfig(String configType) throws IllegalStateException {
-        return read(() -> {
-            Map<String, C> configsMap = (Map) configsCache.getOrDefault(configType, emptyMap());
-            int size = configsMap.size();
-            if (size < 1) {
+        Map<String, AbstractConfig> configsMap = getConfigsMap(configType);
+        int size = configsMap.size();
+        if (size < 1) {
 //                throw new IllegalStateException("No such " + configType.getName() + " is found");
-                return null;
-            } else if (size > 1) {
-                throw new IllegalStateException("Expected single instance of " + configType + ", but found " + size +
-                        " instances, please remove redundant configs. instances: "+configsMap.values());
-            }
-
-            return configsMap.values().iterator().next();
-        });
-    }
-
-    private <V> V write(Callable<V> callable) {
-        V value = null;
-        Lock writeLock = lock.writeLock();
-        try {
-            writeLock.lock();
-            value = callable.call();
-        } catch (RuntimeException e) {
-            throw e;
-        } catch (Throwable e) {
-            throw new RuntimeException(e.getCause());
-        } finally {
-            writeLock.unlock();
-        }
-        return value;
-    }
-
-    private void write(Runnable runnable) {
-        write(() -> {
-            runnable.run();
             return null;
-        });
-    }
-
-    private <V> V read(Callable<V> callable) {
-        Lock readLock = lock.readLock();
-        V value = null;
-        try {
-            readLock.lock();
-            value = callable.call();
-        } catch (Throwable e) {
-            throw new RuntimeException(e);
-        } finally {
-            readLock.unlock();
+        } else if (size > 1) {
+            throw new IllegalStateException("Expected single instance of " + configType + ", but found " + size +
+                " instances, please remove redundant configs. instances: "+configsMap.values());
         }
-        return value;
+        return (C) configsMap.values().iterator().next();
     }
 
     private static boolean isEquals(AbstractConfig oldOne, AbstractConfig newOne) {
@@ -632,7 +588,7 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
     }
 
     private static Map newMap() {
-        return new LinkedHashMap<>();
+        return new ConcurrentHashMap();
     }
 
     /**
@@ -651,7 +607,6 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
         }
 
         // check duplicated configs
-        // TODO Is there any problem with ignoring duplicate and equivalent but different ReferenceConfig instances?
         // special check service and reference config by unique service name, speed up the processing of large number of instances
         if (config instanceof ReferenceConfigBase || config instanceof ServiceConfigBase) {
             C existedConfig = (C) checkDuplicatedInterfaceConfig((AbstractInterfaceConfig) config);
@@ -712,14 +667,11 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
                 key = generateConfigId(config);
             }
 
-        C existedConfig = configsMap.get(key);
-
+        C existedConfig = configsMap.putIfAbsent(key, config);
         if (isEquals(existedConfig, config)) {
             String type = config.getClass().getSimpleName();
             throw new IllegalStateException(String.format("Duplicate %s found, there already has one default %s or more than two %ss have the same id, " +
                     "you can try to give each %s a different id, key: %s, prev: %s, new: %s", type, type, type, type, key, existedConfig, config));
-        } else {
-            configsMap.put(key, config);
         }
         return config;
     }
@@ -760,7 +712,12 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
         AbstractInterfaceConfig prevConfig = configCache.putIfAbsent(uniqueServiceName, config);
         if (prevConfig != null) {
             if (prevConfig == config) {
-                return config;
+                return prevConfig;
+            }
+
+            if (prevConfig.equals(config)) {
+                // TODO Is there any problem with ignoring duplicate and equivalent but different ReferenceConfig instances?
+                return prevConfig;
             }
 
             String configType = config.getClass().getSimpleName();
@@ -781,7 +738,7 @@ public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
 
     public static <C extends AbstractConfig> String generateConfigId(C config) {
         int idx = configIdIndexes.computeIfAbsent(config.getClass(), clazz -> new AtomicInteger(0)).incrementAndGet();
-        return config.getClass().getSimpleName() + "#" + idx;
+        return getTagName(config.getClass()) + "#" + idx;
     }
 
     static <C extends AbstractConfig> String getId(C config) {
diff --git a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java
index 9b3c0b1..e7c0618 100644
--- a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java
+++ b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/ReferenceConfigTest.java
@@ -33,9 +33,13 @@ import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL;
 import static org.apache.dubbo.rpc.Constants.SCOPE_REMOTE;
@@ -51,6 +55,11 @@ public class ReferenceConfigTest {
         this.zkServer = new TestingServer(zkServerPort, true);
         this.zkServer.start();
         this.registryUrl = "zookeeper://localhost:" + zkServerPort;
+
+        // preload
+        ReferenceConfig preloadReferenceConfig = new ReferenceConfig();
+        ApplicationModel.getConfigManager();
+        DubboBootstrap.getInstance();
     }
 
     @AfterEach
@@ -193,25 +202,80 @@ public class ReferenceConfigTest {
     }
 
     @Test
-    public void testLargeReferences() {
-        int amount = 5000;
-        List<ReferenceConfig> referenceConfigs = new ArrayList<>(amount);
-        for (int i = 0; i < amount; i++) {
-            ReferenceConfig referenceConfig = new ReferenceConfig();
-            referenceConfig.setInterface("com.test.TestService" + i);
-            referenceConfigs.add(referenceConfig);
-        }
+    public void testLargeReferences() throws InterruptedException {
+        int amount = 10000;
+        ApplicationConfig applicationConfig = new ApplicationConfig();
+        applicationConfig.setName("test-app");
+        MetadataReportConfig metadataReportConfig = new MetadataReportConfig();
+        metadataReportConfig.setAddress("metadata://");
+        ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();
+        configCenterConfig.setAddress("diamond://");
+
+        testInitReferences(0, amount, applicationConfig, metadataReportConfig, configCenterConfig);
+        ApplicationModel.getConfigManager().clear();
+        testInitReferences(0, 1, applicationConfig, metadataReportConfig, configCenterConfig);
 
-        // test add large number of references
         long t1 = System.currentTimeMillis();
-        for (ReferenceConfig referenceConfig : referenceConfigs) {
-            DubboBootstrap.getInstance().reference(referenceConfig);
+        int nThreads = 8;
+        ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
+        for(int i=0;i<nThreads;i++) {
+            int perCount = (int) (1.0* amount / nThreads);
+            int start = perCount * i;
+            int end = start + perCount;
+            if (i == nThreads - 1) {
+                end = amount;
+            }
+            int finalEnd = end;
+            System.out.println(String.format("start thread %s: range: %s - %s, count: %s", i, start, end, (end-start)));
+            executorService.submit(()->{
+                testInitReferences(start, finalEnd, applicationConfig, metadataReportConfig, configCenterConfig);
+            });
         }
+        executorService.shutdown();
+        executorService.awaitTermination(100, TimeUnit.SECONDS);
+
         long t2 = System.currentTimeMillis();
         long cost = t2 - t1;
-        System.out.println("Add large references cost: " + cost + "ms");
+        System.out.println("Init large references cost: " + cost + "ms");
         Assertions.assertEquals(amount, DubboBootstrap.getInstance().getConfigManager().getReferences().size());
-        Assertions.assertTrue( cost < 500, "add large reference too slowly: "+cost);
+        Assertions.assertTrue( cost < 1000, "Init large references too slowly: "+cost);
+
+        //test equals
+        testSearchReferences();
+
+    }
+
+    private void testSearchReferences() {
+        long t1 = System.currentTimeMillis();
+        Collection<ReferenceConfigBase<?>> references = DubboBootstrap.getInstance().getConfigManager().getReferences();
+        List<ReferenceConfigBase<?>> results = references.stream().filter(rc -> rc.equals(references.iterator().next()))
+            .collect(Collectors.toList());
+        long t2 = System.currentTimeMillis();
+        long cost = t2 - t1;
+        System.out.println("Search large references cost: " + cost + "ms");
+        Assertions.assertEquals(1, results.size());
+        Assertions.assertTrue( cost < 1000, "Search large references too slowly: "+cost);
+    }
+
+    private long testInitReferences(int start, int end, ApplicationConfig applicationConfig, MetadataReportConfig metadataReportConfig, ConfigCenterConfig configCenterConfig) {
+        // test add large number of references
+        long t1 = System.currentTimeMillis();
+        try {
+            for (int i = start; i < end; i++) {
+                ReferenceConfig referenceConfig = new ReferenceConfig();
+                referenceConfig.setInterface("com.test.TestService" + i);
+                referenceConfig.setApplication(applicationConfig);
+                referenceConfig.setMetadataReportConfig(metadataReportConfig);
+                referenceConfig.setConfigCenter(configCenterConfig);
+                DubboBootstrap.getInstance().reference(referenceConfig);
+
+                //ApplicationModel.getConfigManager().getConfigCenters();
+            }
+        } catch (Throwable e) {
+            e.printStackTrace();
+        }
+        long t2 = System.currentTimeMillis();
+        return t2 - t1;
     }
 
     @Test