Apache Dubbo学习实战-缓存机制

978 阅读1分钟

缓存机制

  缓存机制就是用空间来换取时间的机制,如果每次都要从远程注册中心获取一次可以调用的服务列表,则会让注册中心承受巨大的网络压力和流量压力。因此Dubbo的注册中心实现了通用的缓存机制,在抽象AbstractRegistry中实现。AbstractRegistry类结构关系如下图所示

image.png

  消费者或者服务治理中心获取注册信息后会做本地缓存。内存中会有一份,保存在Properties对象里,磁盘上也会持久化一份文件,通过file对象引用。在AbstractRegistry抽象类中有如下的定义。

  // Local disk cache, where the special key value.registries records the list of registry centers, and the others are the list of notified service providers
    private final Properties properties = new Properties();
    private URL registryUrl;
    // Local disk cache file
    private File file;
    
    private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<>();
    private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<>();

  内存中的缓存notified是ConcurrentHashMap里面嵌套了一个Map,外层Map的key是消费者URL,内层Map是Key是分类,包含providers、consumers、routes、configurators四种。value则是对应的服务列表,对于没有服务提供者提供服务的URL,会以特殊的empty://前缀开头。

缓存的加载

  在服务初始化的时候,AbstractRegistry构造函数里会从本地磁盘文件中把持久化的注册数据读取到Properties中,并且加载到内存中。

  private void loadProperties() {
        if (file != null && file.exists()) {
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                properties.load(in);
                if (logger.isInfoEnabled()) {
                    logger.info("Load registry cache file " + file + ", data: " + properties);
                }
            } catch (Throwable e) {
                logger.warn("Failed to load registry cache file " + file, e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        logger.warn(e.getMessage(), e);
                    }
                }
            }
        }
    }

  Properties保存了所有服务提供者的URL,使用URL#serviceKey()作为key,提供者列表、路由规则列表、配置规则列表等作为value。由于value是列表,当存在多个的时候使用空格隔开。还有一个特殊的key.registies,保存所有的注册中心地址,如果应用在启动的过程中,注册中心无法连接或者是宕机了,则Dubbo框架会自动通过本地缓存加载Invokers。

缓存的保存与更新

  缓存的保存有同步和异步两种方式。异步会使用线程池异步保存,如果线程在执行过程中出现异常,则会再次调用线程池不断进行重试

    private void saveProperties(URL url) {
        if (file == null) {
            return;
        }

        try {
            StringBuilder buf = new StringBuilder();
            Map<String, List<URL>> categoryNotified = notified.get(url);
            if (categoryNotified != null) {
                for (List<URL> us : categoryNotified.values()) {
                    for (URL u : us) {
                        if (buf.length() > 0) {
                            buf.append(URL_SEPARATOR);
                        }
                        buf.append(u.toFullString());
                    }
                }
            }
            properties.setProperty(url.getServiceKey(), buf.toString());
            long version = lastCacheChanged.incrementAndGet();
            if (syncSaveFile) {
                doSaveProperties(version);
            } else {
                registryCacheExecutor.execute(new SaveProperties(version));
            }
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
    }

  AbstractRegistry#notify 方法中封装了更新缓存和更新文件的逻辑。当客户端第一次订阅获取全量数据,或者后续由于订阅得到新数据的时候,都会调用该方法进行保存

    protected void notify(List<URL> urls) {
        if (CollectionUtils.isEmpty(urls)) {
            return;
        }

        for (Map.Entry<URL, Set<NotifyListener>> entry : getSubscribed().entrySet()) {
            URL url = entry.getKey();

            if (!UrlUtils.isMatch(url, urls.get(0))) {
                continue;
            }

            Set<NotifyListener> listeners = entry.getValue();
            if (listeners != null) {
                for (NotifyListener listener : listeners) {
                    try {
                        notify(url, listener, filterEmpty(url, urls));
                    } catch (Throwable t) {
                        logger.error("Failed to notify registry event, urls: " + urls + ", cause: " + t.getMessage(), t);
                    }
                }
            }
        }
    }