开篇

  • 根据官方图,dubbo调用者需要通过注册中心(例如:ZK)注册信息,获取提供者,但是如果频繁往ZK获取信息,肯定会存在单点故障问题,所以dubbo提供了将提供者信息缓存在本地的方法。
  • Dubbo在订阅注册中心的回调处理逻辑当中会保存服务提供者信息到本地缓存文件当中(同步/异步两种方式),以url纬度进行全量保存。
  • Dubbo在服务引用过程中会创建registry对象并加载本地缓存文件,会优先订阅注册中心,订阅注册中心失败后会访问本地缓存文件内容获取服务提供信息。

核心实现类

注册中心

  • 核心的实现类是AbstractRegistry。

AbstractRegistry

notify

public abstract class AbstractRegistry implements Registry {

    // URL address separator, used in file cache, service provider URL separation
    private static final char URL_SEPARATOR = ' ';
    // URL address separated regular expression for parsing the service provider URL list in the file cache
    private static final String URL_SPLIT = "\\s+";
    // Log output
    protected final Logger logger = LoggerFactory.getLogger(getClass());
    // Local disk cache, where the special key value.registies records the list of registry centers, and the others are the list of notified service providers
    private final Properties properties = new Properties();
    // File cache timing writing
    private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
    // Is it synchronized to save the file
    private final boolean syncSaveFile;
    private final AtomicLong lastCacheChanged = new AtomicLong();
    private final Set<URL> registered = new ConcurrentHashSet<URL>();
    private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();
    private URL registryUrl;
    // 本地缓存文件对象
    private File file;

    public AbstractRegistry(URL url) {
        setUrl(url);
        // Start file save timer
        syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
        // 本地缓存文件名
        String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");

        File file = null;
        if (ConfigUtils.isNotEmpty(filename)) {
            file = new File(filename);
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {
                    throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
                }
            }
        }
        this.file = file;
        loadProperties();
        notify(url.getBackupUrls());
    }

    // 回调通知处理函数
    protected void notify(URL url, NotifyListener listener, List<URL> urls) {
        if (url == null) {
            throw new IllegalArgumentException("notify url == null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("notify listener == null");
        }
        if ((urls == null || urls.isEmpty())
                && !Constants.ANY_VALUE.equals(url.getServiceInterface())) {
            logger.warn("Ignore empty notify urls for subscribe url " + url);
            return;
        }
        if (logger.isInfoEnabled()) {
            logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
        }
        Map<String, List<URL>> result = new HashMap<String, List<URL>>();
        for (URL u : urls) {
            if (UrlUtils.isMatch(url, u)) {
                String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
                List<URL> categoryList = result.get(category);
                if (categoryList == null) {
                    categoryList = new ArrayList<URL>();
                    result.put(category, categoryList);
                }
                categoryList.add(u);
            }
        }

        if (result.size() == 0) {
            return;
        }

        Map<String, List<URL>> categoryNotified = notified.get(url);
        if (categoryNotified == null) {
            notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
            categoryNotified = notified.get(url);
        }
        for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
            String category = entry.getKey();
            List<URL> categoryList = entry.getValue();
            categoryNotified.put(category, categoryList);
            // 保存配置信息到本地文件
            saveProperties(url);
            listener.notify(categoryList);
        }
    }
}
  • 注册中心的在服务变更过程中通过回调函数notify进行通知。
  • 在通知回调中通过saveProperties()方法进行属性保存。
  • AbstractRegistry的构造函数当中会创建本地缓存的持久化文件名。
     

saveProperties

public abstract class AbstractRegistry implements Registry {

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

        try {
            // 将url的所有属性进行拼接,通过空格进行拼接。
            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设置属性
            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);
        }
    }


    private class SaveProperties implements Runnable {
        private long version;

        private SaveProperties(long version) {
            this.version = version;
        }

        @Override
        public void run() {
            doSaveProperties(version);
        }
    }
}
  • 通过properties.setProperty(url.getServiceKey(), buf.toString())保存服务的属性。
  • 持久化到文件分为同步和异步,异步通过线程SaveProperties异步执行,同步调用doSaveProperties直接写文件。

doSaveProperties

public abstract class AbstractRegistry implements Registry {

    public void doSaveProperties(long version) {
        if (version < lastCacheChanged.get()) {
            return;
        }
        if (file == null) {
            return;
        }
        // Save
        try {
            File lockfile = new File(file.getAbsolutePath() + ".lock");
            if (!lockfile.exists()) {
                lockfile.createNewFile();
            }
            RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
            try {
                FileChannel channel = raf.getChannel();
                try {
                    FileLock lock = channel.tryLock();
                    if (lock == null) {
                        throw new IOException("Can not lock the registry cache file " + file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
                    }
                    // Save
                    try {
                        if (!file.exists()) {
                            file.createNewFile();
                        }
                        FileOutputStream outputFile = new FileOutputStream(file);
                        try {
                            properties.store(outputFile, "Dubbo Registry Cache");
                        } finally {
                            outputFile.close();
                        }
                    } finally {
                        lock.release();
                    }
                } finally {
                    channel.close();
                }
            } finally {
                raf.close();
            }
        } catch (Throwable e) {
            if (version < lastCacheChanged.get()) {
                return;
            } else {
                registryCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
            }
            logger.warn("Failed to save registry store file, cause: " + e.getMessage(), e);
        }
    }
}
  • doSaveProperties实现持久化动作。
  • <dubbo:registry>标签包含file属性,指定本地文件缓存的文件名。
  • 使用文件缓存注册中心地址列表及服务提供者列表,应用重启时将基于此文件恢复,注意:两个注册中心不能使用同一文件存储。
  • doSaveProperties执行真正的属性保存,通过文件的lock属性进行互斥实现。

本地缓存文件

所在目录 
/Users/lebron374/.dubbo

文件
dubbo-registry-demo-consumer-127.0.0.1:2181.cache
dubbo-registry-demo-consumer-127.0.0.1:2181.cache.lock
dubbo-registry-demo-provider-127.0.0.1:2181.cache
dubbo-registry-demo-provider-127.0.0.1:2181.cache.lock

文件内容
#Dubbo Registry Cache
#Wed Jan 22 20:59:44 CST 2020
com.alibaba.dubbo.demo.DemoService=empty\://192.168.1.106/com.alibaba.dubbo.demo.DemoService?application\=demo-consumer&category\=configurators&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.DemoService&methods\=sayHello&pid\=31961&qos.port\=33333&side\=consumer×tamp\=1579697802111 empty\://192.168.1.106/com.alibaba.dubbo.demo.DemoService?application\=demo-consumer&category\=routers&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.DemoService&methods\=sayHello&pid\=31961&qos.port\=33333&side\=consumer×tamp\=1579697802111 dubbo\://192.168.1.106\:20881/com.alibaba.dubbo.demo.DemoService?anyhost\=true&application\=demo-provider&bean.name\=com.alibaba.dubbo.demo.DemoService&dubbo\=2.0.2&generic\=false&interface\=com.alibaba.dubbo.demo.DemoService&methods\=sayHello&pid\=31565&side\=provider×tamp\=1579696923081 empty\://192.168.1.106/com.alibaba.dubbo.demo.DemoService?application\=demo-consumer&category\=providers,configurators,routers&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.DemoService&methods\=sayHello&pid\=31961&qos.port\=33333&side\=consumer×tamp\=1579697802111
com.alibaba.dubbo.demo.AnotherService=empty\://192.168.1.106/com.alibaba.dubbo.demo.AnotherService?application\=demo-consumer&category\=configurators&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.AnotherService&methods\=sayHello&pid\=31796&qos.port\=33333&side\=consumer×tamp\=1579697435025 empty\://192.168.1.106/com.alibaba.dubbo.demo.AnotherService?application\=demo-consumer&category\=routers&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.AnotherService&methods\=sayHello&pid\=31796&qos.port\=33333&side\=consumer×tamp\=1579697435025 dubbo\://192.168.1.106\:20881/com.alibaba.dubbo.demo.AnotherService?anyhost\=true&application\=demo-provider&bean.name\=com.alibaba.dubbo.demo.AnotherService&dubbo\=2.0.2&generic\=false&interface\=com.alibaba.dubbo.demo.AnotherService&methods\=sayHello&pid\=31565&side\=provider×tamp\=1579697270262
com.alibaba.dubbo.demo.AnotherServiceV2=empty\://192.168.1.106/com.alibaba.dubbo.demo.AnotherServiceV2?application\=demo-consumer&category\=configurators&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.AnotherServiceV2&methods\=sayHello&pid\=31796&qos.port\=33333&side\=consumer×tamp\=1579697439692 empty\://192.168.1.106/com.alibaba.dubbo.demo.AnotherServiceV2?application\=demo-consumer&category\=routers&check\=false&dubbo\=2.0.2&interface\=com.alibaba.dubbo.demo.AnotherServiceV2&methods\=sayHello&pid\=31796&qos.port\=33333&side\=consumer×tamp\=1579697439692 dubbo\://192.168.1.106\:20881/com.alibaba.dubbo.demo.AnotherServiceV2?anyhost\=true&application\=demo-provider&bean.name\=com.alibaba.dubbo.demo.AnotherServiceV2&dubbo\=2.0.2&generic\=false&interface\=com.alibaba.dubbo.demo.AnotherServiceV2&methods\=sayHello&pid\=31565&side\=provider×tamp\=1579697270512

缓存加载

public abstract class FailbackRegistry extends AbstractRegistry {
    public void subscribe(URL url, NotifyListener listener) {
        super.subscribe(url, listener);
        removeFailedSubscribed(url, listener);
        try {
            // Sending a subscription request to the server side
            doSubscribe(url, listener);
        } catch (Exception e) {
            Throwable t = e;
            // 注册中心订阅失败后读取本地缓存文件获取服务
            List<URL> urls = getCacheUrls(url);
            if (urls != null && !urls.isEmpty()) {
                notify(url, listener, urls);
                logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
            } else {
                // If the startup detection is opened, the Exception is thrown directly.
                boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                        && url.getParameter(Constants.CHECK_KEY, true);
                boolean skipFailback = t instanceof SkipFailbackWrapperException;
                if (check || skipFailback) {
                    if (skipFailback) {
                        t = t.getCause();
                    }
                    throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
                } else {
                    logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
                }
            }

            // Record a failed registration request to a failed list, retry regularly
            addFailedSubscribed(url, listener);
        }
    }
}
  • 优先订阅注册中心获取服务提供者,如果注册中心访问异常,获取本地缓存的服务提供者。
public abstract class AbstractRegistry implements Registry {

    public AbstractRegistry(URL url) {
        setUrl(url);
        // Start file save timer
        syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
        String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
        File file = null;
        if (ConfigUtils.isNotEmpty(filename)) {
            file = new File(filename);
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {
                    throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
                }
            }
        }
        this.file = file;
        // registry对象创建的时候加载本地缓存对象
        loadProperties();
        notify(url.getBackupUrls());
    }

    private void loadProperties() {
        if (file != null && file.exists()) {
            InputStream in = null;
            try {
                in = new FileInputStream(file);
                // 本地缓存文件保存至properties当中
                properties.load(in);
                if (logger.isInfoEnabled()) {
                    logger.info("Load registry store file " + file + ", data: " + properties);
                }
            } catch (Throwable e) {
                logger.warn("Failed to load registry store file " + file, e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        logger.warn(e.getMessage(), e);
                    }
                }
            }
        }
    }


    public List<URL> getCacheUrls(URL url) {
        // properties保存的是本地缓存文件内容
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            if (key != null && key.length() > 0 && key.equals(url.getServiceKey())
                    && (Character.isLetter(key.charAt(0)) || key.charAt(0) == '_')
                    && value != null && value.length() > 0) {
                String[] arr = value.trim().split(URL_SPLIT);
                List<URL> urls = new ArrayList<URL>();
                for (String u : arr) {
                    urls.add(URL.valueOf(u));
                }
                return urls;
            }
        }
        return null;
    }
}
  • 在创建Registry对象的构造函数当中会加载本地缓存文件并保存至properties对象当中。
  • getCacheUrls负责根据url获取缓存的服务提供者信息。

3人点赞

dubbo