Dubbo2.6.x—注冊中心源碼分析 dubbo-registry模塊 (api and zookeeper)

文章有點長,親,要慢慢看!

1. 概述

1.1 注冊中心作用

  • 在Dubbo中,注冊中心為核心模塊,Dubbo通過注冊中心實現各個服務之間的注冊與發現等功能,而本次源碼的分析為registry模塊的api和zookeeper的實現。
  • 服務的提供者和消費者都需要把自己注冊到注冊中心,提供者讓消費者感知到服務存在,從而消費者發起遠程調用,也讓服務治理中心感知到有服務提供者上線;消費者則是讓服務治理中心可以發現自己。

1.2 Zookeeper

  • Zookeeper是一個提供分布式協調服務的開源軟件,常用于解決分布式應用中經常遇到的一些數據管理問題。Zookeeper功能非常強大,可以實現如分布式應用配置管理、統一命名服務、狀態同步服務、集群管理等功能。關于Zookeeper,大家如果想了解可以關注一下自行去搜索一下。

1.3 registry模塊

  • 整個registry下的模塊


    dubbo-registry
  • api是注冊中心所有的API和抽象類實現

  • default是注冊中心的內存實現

  • zookeeper、redis、nacos就是基于不同的組件的實現

  • multicast是通過廣播實現

1.4 注冊中心工作流程

image

這張圖相信只要是用過的都不陌生,掛在dubbo.io的官網掛了很久很久了。那么這個流程主要是說了什么呢?

  • 0.是生產者(服務提供方)初始化,就好比你寫了個服務實現然后啟動起來。
  • 1.是服務提供方向啟動器起來過后,就會向注冊中心提交自己的服務信息
  • 2.是消費者(服務消費方)向注冊中心提交訂閱請求。就是你寫了一個業務需要用到一個生產者服務,這個時候你需要提前打招呼,我需要它,有它的消息的時候讓注冊中心告訴你他的信息。
  • 3.這個時候當服務提供者離開或者是有新的服務提供者加入,注冊中心就會將變化的信息發送給消費者。
  • 4.消費者知道了生產者的信息,要用的時候就直接調用,注意這里的調用是不經過注冊中心的,而是直接同步的網絡調用。

2. dubbo-registry-api

  • api層主要是注冊中心所有API的抽象實現類,并不是實際提供服務的組件。
  • 模塊關系圖


    image
  • 類關系圖


    image
  • 目錄結構


    image

2.1 Registry的相關實現

  • 由類的關系圖科看到Registry的實現關系,我們接下來就分析下各個接口和這個類
image

2.1.1 RegistryService

  • 注冊中心模塊的服務接口:提供了注冊、取消注冊、訂閱、取消訂閱、查詢符合條件的已注冊數據。
  • 雖然官方有解釋這個的地方但是還是復制一下方法解釋如下,官方地址是:http://dubbo.apache.org/zh-cn/docs/dev/impls/registry.html
public interface RegistryService {
    /**
     * 注冊服務.
     * @param url 注冊信息,不允許為空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     */
    void register(URL url);
 
    /**
     * 取消注冊服務.
     * @param url 注冊信息,不允許為空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     */
    void unregister(URL url);
 
    /**
     * 訂閱服務.
     * @param listener 變更事件監聽器,不允許為空
     */
    void subscribe(URL url, NotifyListener listener);
 
    /**
     * 取消訂閱服務.
     * @param url 訂閱條件,不允許為空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @param listener 變更事件監聽器,不允許為空
     */
    void unsubscribe(URL url, NotifyListener listener);
 
    /**
     * 查詢注冊列表,與訂閱的推模式相對應,這里為拉模式,只返回一次結果。
     * 
     * @see org.apache.dubbo.registry.NotifyListener#notify(List)
     * @param url 查詢條件,不允許為空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
     * @return 已注冊信息列表,可能為空,含義同{@link org.apache.dubbo.registry.NotifyListener#notify(List<URL>)}的參數。
     */
    List<URL> lookup(URL url);
}

2.1.2 Node (不在api中定義,在common模塊中)

  • 節點的接口 里面聲明了一些關于節點的操作方法
public interface Node {

    /**
     * 獲取節點Url
     */
    URL getUrl();

    /**
     * 是否可用
     */
    boolean isAvailable();

    /**
     * 銷毀節點
     */
    void destroy();

}

2.1.2 Registry

  • 這個接口其實就是把節點以及注冊中心服務的方法放在了一起
public interface Registry extends Node, RegistryService {
}

2.1.3 AbstractRegistry

  • AbstractRegistry實現了Registry接口,為減輕注冊中心的壓力,在該類中實現了把本地URL緩存到property文件中的機制,并且實現了注冊中心的注冊、訂閱等方法。
  • 看下類圖
image
  • 首先是抽象類的屬性
    // url地址分隔符,用于文件緩存,服務提供程序url分隔
    private static final char URL_SEPARATOR = ' ';
    // URL地址分隔的正則表達式,用于分析文件緩存中的服務提供程序URL列表
    private static final String URL_SPLIT = "\\s+";
    // 日志輸出
    protected final Logger logger = LoggerFactory.getLogger(getClass());
    // 本地磁盤緩存,其中的特殊key為registies是記錄注冊表中心列表,其他是服務提供者的李彪
    private final Properties properties = new Properties();
    // 文件緩存寫入執行器 提供一個線程的線程池 
    private final ExecutorService registryCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory("DubboSaveRegistryCache", true));
    // 是否同步保存文件標志
    private final boolean syncSaveFile;
    // 這個是緩存的版本號
    private final AtomicLong lastCacheChanged = new AtomicLong();
    // 這個是已經注冊的URL集合,不僅僅是服務提供者的,也可以是服務消費者的
    private final Set<URL> registered = new ConcurrentHashSet<URL>();

    // 已訂閱的url 值為url的監聽器集合
    private final ConcurrentMap<URL, Set<NotifyListener>> subscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
   
    // 消費者或服務治理服務獲取注冊信息后的緩存對象
    // 內存中服務器緩存的notified對象是ConcurrentHashMap里面嵌套了一個Map,
    // 外層Map的Key是消費者的URL,
    // 內層的Map的key是分類,包括provider,consumer,routes,configurators四種,
    // value則對應服務列表,沒有服務提供者提供服務的URL,會以一個特別的empty://前綴開頭
    private final ConcurrentMap<URL, Map<String, List<URL>>> notified = new ConcurrentHashMap<URL, Map<String, List<URL>>>();
    // 注冊中心的URL
    private URL registryUrl;
    // 本地磁盤緩存文件保存的是注冊中心的數據
    private File file;
2.1.3.1 構造方法
    public AbstractRegistry(URL url) {
        // 設置注冊中心的地址URL
        setUrl(url);
        // 從URL參數中獲取是否同步保存的狀態,URL中如果不包含,那就設置默認值為false
        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;
        // 加載文件中的參數放入Properties,Properties繼承HashTable。
        loadProperties();
        // 通知監聽器 URL變化 見下面notify的源碼
        notify(url.getBackupUrls());
    }
private void loadProperties() {
        if (file != null && file.exists()) {
            InputStream in = null;
            try {
                // 把文件中的key-value讀進來
                in = new FileInputStream(file);
                // Properties是一個繼承HashTable的類.
                // 這個地方就是按行讀入,util里面的類,里面調用了一個load0 方法會把key和value做分割然后放入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);
                    }
                }
            }
        }
    }
2.1.3.2 lookup
  • 獲得消費者url訂閱的服務URL列表
    @Override
    public List<URL> lookup(URL url) {
        // 查找的結果數據
        List<URL> result = new ArrayList<URL>();
        // 獲取注冊信息中的分類服務列表信息
        Map<String, List<URL>> notifiedUrls = getNotified().get(url);
        // 如果該消費者訂閱了服務
        if (notifiedUrls != null && notifiedUrls.size() > 0) {
            for (List<URL> urls : notifiedUrls.values()) {
                for (URL u : urls) {
                    // 把非空的加入結果集中
                    if (!Constants.EMPTY_PROTOCOL.equals(u.getProtocol())) {
                        result.add(u);
                    }
                }
            }
        } else {
            // 如果沒有訂閱服務
            // 使用原子類以保證在獲取注冊在注冊中心的服務url時能夠保證是最新的url集合
            final AtomicReference<List<URL>> reference = new AtomicReference<List<URL>>();
            // 通知監聽器。當收到服務變更通知時觸發
            NotifyListener listener = new NotifyListener() {
                @Override
                public void notify(List<URL> urls) {
                    reference.set(urls);
                }
            };
            // 添加這個服務的監聽器
            subscribe(url, listener); // Subscribe logic guarantees the first notify to return
            List<URL> urls = reference.get();
            // 然后把非空結果放入結果集中
            if (urls != null && !urls.isEmpty()) {
                for (URL u : urls) {
                    if (!Constants.EMPTY_PROTOCOL.equals(u.getProtocol())) {
                        result.add(u);
                    }
                }
            }
        }
        return result;
    }
2.1.3.3 register and unregister
  • url注冊和取消注冊代碼很簡單,就是向registered中add或者remove url
    @Override
    public void register(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("register url == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Register: " + url);
        }
        registered.add(url);
    }

    @Override
    public void unregister(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("unregister url == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Unregister: " + url);
        }
        registered.remove(url);
    }
2.1.3.4 notify
    protected void notify(List<URL> urls) {
        if (urls == null || urls.isEmpty()) return;
        // 遍歷已訂閱的URL
        for (Map.Entry<URL, Set<NotifyListener>> entry : getSubscribed().entrySet()) {
            URL url = entry.getKey();

            if (!UrlUtils.isMatch(url, urls.get(0))) {
                continue;
            }
            // 通知URL對應的監聽器
            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);
                    }
                }
            }
        }
    }
    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>>();
        // 將url進行分類
        for (URL u : urls) {
            if (UrlUtils.isMatch(url, u)) {
                // 根據不同的category分別放到不同List中處理 以category的值做分類
                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);
            }
        }
        // 沒有分類結果就直接return
        if (result.size() == 0) {
            return;
        }
        // 獲得消費者被通知的url的Map
        Map<String, List<URL>> categoryNotified = notified.get(url);
        // 如果沒有 就創建一個
        if (categoryNotified == null) {
            notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
            // 創建過后再獲取
            categoryNotified = notified.get(url);
        }
        // 發送URL變化給監聽器
        for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
            String category = entry.getKey();
            List<URL> categoryList = entry.getValue();
            // 把分類標實和分類后的列表放入notified的value中 覆蓋到 `notified`
            // 當分類的數據為空,依然有urls 。不過其中的urls[0].protocol是empty,以此來處理所有服務提供者為空時的情況。
            categoryNotified.put(category, categoryList);
            // 保存一份到文件緩存中 中間做的 就是解析出參數然后同步或者異步保存到文件中
            saveProperties(url);
            // 通知監聽器
            listener.notify(categoryList);
        }
    }
2.1.3.5 subscribe and unsubscribe
  • 注冊中心服務實現的訂閱和取消訂閱

    @Override
    public void subscribe(URL url, NotifyListener listener) {
        if (url == null) {
            throw new IllegalArgumentException("subscribe url == null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("subscribe listener == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Subscribe: " + url);
        }
        //  獲得url已經訂閱的服務的監聽器集合
        Set<NotifyListener> listeners = subscribed.get(url);
        if (listeners == null) {
            subscribed.putIfAbsent(url, new ConcurrentHashSet<NotifyListener>());
            listeners = subscribed.get(url);
        }
        // 然后把listener添加到上
        listeners.add(listener);
    }

    @Override
    public void unsubscribe(URL url, NotifyListener listener) {
        if (url == null) {
            throw new IllegalArgumentException("unsubscribe url == null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("unsubscribe listener == null");
        }
        if (logger.isInfoEnabled()) {
            logger.info("Unsubscribe: " + url);
        }
        //  獲得url已經訂閱的服務的監聽器集合
        Set<NotifyListener> listeners = subscribed.get(url);
        if (listeners != null) {
            // 然后移除
            listeners.remove(listener);
        }
    }
2.1.3.6 recover
  • 注冊中心的連接斷開后恢復時調用的方法,里面其實就是注冊和訂閱
    protected void recover() throws Exception {
        // register
        Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
        if (!recoverRegistered.isEmpty()) {
            if (logger.isInfoEnabled()) {
                logger.info("Recover register url " + recoverRegistered);
            }
            for (URL url : recoverRegistered) {
                //調用的上面的注冊方法
                register(url);
            }
        }
        // subscribe
        Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
        if (!recoverSubscribed.isEmpty()) {
            if (logger.isInfoEnabled()) {
                logger.info("Recover subscribe url " + recoverSubscribed.keySet());
            }
            for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
                URL url = entry.getKey();
                for (NotifyListener listener : entry.getValue()) {
                    // 調用上面的訂閱方法
                    subscribe(url, listener);
                }
            }
        }
    }
2.1.3.7 destory
  • 這個方法是在進程關閉時,去取消注冊和訂閱,實際上就是調用unregister和unsubscribe
    @Override
    public void destroy() {
        if (logger.isInfoEnabled()) {
            logger.info("Destroy registry:" + getUrl());
        }
        // 獲取以注冊的URL
        Set<URL> destroyRegistered = new HashSet<URL>(getRegistered());
        if (!destroyRegistered.isEmpty()) {
            for (URL url : new HashSet<URL>(getRegistered())) {
                if (url.getParameter(Constants.DYNAMIC_KEY, true)) {
                    try {
                        // 取消注冊
                        unregister(url);
                        if (logger.isInfoEnabled()) {
                            logger.info("Destroy unregister url " + url);
                        }
                    } catch (Throwable t) {
                        logger.warn("Failed to unregister url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
                    }
                }
            }
        }
        // 獲取已訂閱的URL以及監聽器
        Map<URL, Set<NotifyListener>> destroySubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
        if (!destroySubscribed.isEmpty()) {
            for (Map.Entry<URL, Set<NotifyListener>> entry : destroySubscribed.entrySet()) {
                URL url = entry.getKey();
                for (NotifyListener listener : entry.getValue()) {
                    try {
                        // 去取消訂閱
                        unsubscribe(url, listener);
                        if (logger.isInfoEnabled()) {
                            logger.info("Destroy unsubscribe url " + url);
                        }
                    } catch (Throwable t) {
                        logger.warn("Failed to unsubscribe url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
                    }
                }
            }
        }
    }

2.1.4 FailbackRegistry

  • 這個類其實是為AbstractRegistry增加了失敗重試的機制作為抽象能力,后面不同的注冊中心具體實現繼承了這個類就可以直接使用這個能力。
  • 類圖


    image
  • 常規套路 類的屬性
    // Scheduled executor service
    // 經過固定時間后(默認是5s),調用FailbackRegistry#retry方法
    private final ScheduledExecutorService retryExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("DubboRegistryFailedRetryTimer", true));

    // Timer for failure retry, regular check if there is a request for failure, and if there is, an unlimited retry
    // 失敗重試計時器,定期檢查是否有失敗請求,如果有,則無限制重試
    private final ScheduledFuture<?> retryFuture;
    // 注冊失敗的集合
    private final Set<URL> failedRegistered = new ConcurrentHashSet<URL>();
    // 取消注冊失敗的集合
    private final Set<URL> failedUnregistered = new ConcurrentHashSet<URL>();
    // 發起訂閱失敗的監聽器集合
    private final ConcurrentMap<URL, Set<NotifyListener>> failedSubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    // 取消訂閱失敗的監聽器集合
    private final ConcurrentMap<URL, Set<NotifyListener>> failedUnsubscribed = new ConcurrentHashMap<URL, Set<NotifyListener>>();
    // 通知失敗的URL集合
    private final ConcurrentMap<URL, Map<NotifyListener, List<URL>>> failedNotified = new ConcurrentHashMap<URL, Map<NotifyListener, List<URL>>>();
    /**
     * The time in milliseconds the retryExecutor will wait
     * RetryExecutor將等待的時間(毫秒)
     */
    private final int retryPeriod;
2.1.4.1 構造方法
    public FailbackRegistry(URL url) {
        super(url);
        // 獲取重試的時間 如果沒有就設置成默認的 DEFAULT_REGISTRY_RETRY_PERIOD = 5 * 1000;
        this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
        // 設置重試任務 里面就是調用retry方法 見下方retry方法的解析
        this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                // Check and connect to the registry
                try {
                    retry();
                } catch (Throwable t) { // Defensive fault tolerance
                    logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
                }
            }
        }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
    }
2.1.4.2 register and unregister 、 subscribe and unsubscribe
  • 注冊和取消注冊
    @Override
    public void register(URL url) {
        // 緩存等注冊操作 見AbstractRegistry
        super.register(url);
        // 在失敗集合中將這個移除
        failedRegistered.remove(url);
        failedUnregistered.remove(url);
        try {
            // Sending a registration request to the server side
            // 向服務器發送注冊請求
            doRegister(url);
        } catch (Exception e) {
            Throwable t = e;

            // 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)
                    && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
            boolean skipFailback = t instanceof SkipFailbackWrapperException;
            if (check || skipFailback) {
                if (skipFailback) {
                    t = t.getCause();
                }
                throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
            } else {
                logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
            }

            // Record a failed registration request to a failed list, retry regularly
            // 記錄失敗的url
            failedRegistered.add(url);
        }
    }
  • 后面的unregister方法,subscribe unsubscribe都類似 可以看下源碼, 中間的doXXXX這幾個方法都是abstract方法等著后面不同的服務來實現。
2.1.4.3 notify
  • notify則與上面的 四個方法不同,它是默認調用的父類AbstractRegistry的notify方法

    @Override
    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");
        }
        try {
            doNotify(url, listener, urls);
        } catch (Exception t) {
            // Record a failed registration request to a failed list, retry regularly
            // 將失敗的注冊請求記錄到失敗列表,定期重試
            Map<NotifyListener, List<URL>> listeners = failedNotified.get(url);
            if (listeners == null) {
                failedNotified.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, List<URL>>());
                listeners = failedNotified.get(url);
            }
            listeners.put(listener, urls);
            logger.error("Failed to notify for subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
        }
    }

    protected void doNotify(URL url, NotifyListener listener, List<URL> urls) {
        // 注意 這個是調用父類的
        super.notify(url, listener, urls);
    }

2.1.4.4 recover
  • recover方法也區別于AbstractRegistry,他是直接添加到失敗重試的集合中,讓定時任務自己去重新注冊和訂閱
@Override
    protected void recover() throws Exception {
        // register
        // 把已注冊的添加到失敗重試的列表中
        Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
        if (!recoverRegistered.isEmpty()) {
            if (logger.isInfoEnabled()) {
                logger.info("Recover register url " + recoverRegistered);
            }
            for (URL url : recoverRegistered) {
                // 添加到失敗重試注冊列表
                failedRegistered.add(url);
            }
        }
        // subscribe
        // 把已訂閱的添加到失敗重試的列表中
        Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
        if (!recoverSubscribed.isEmpty()) {
            if (logger.isInfoEnabled()) {
                logger.info("Recover subscribe url " + recoverSubscribed.keySet());
            }
            for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
                URL url = entry.getKey();
                for (NotifyListener listener : entry.getValue()) {
                    // 添加到失敗重試訂閱列表
                    addFailedSubscribed(url, listener);
                }
            }
        }
    }
2.1.4.5 retry
  • 重試的方法,其實也比較簡單,就是把集合中的數據拿出來,該做注冊做注冊,該訂閱就訂閱,成功了就從失敗重試集合中移除,失敗了就等下次再來。簡單看下對注冊列表的代碼就明白了。其他代碼都是類似的
// Retry the failed actions
    protected void retry() {
        if (!failedRegistered.isEmpty()) {
            // 不為空就把他URL拿到
            Set<URL> failed = new HashSet<URL>(failedRegistered);
            if (failed.size() > 0) {
                if (logger.isInfoEnabled()) {
                    logger.info("Retry register " + failed);
                }
                try {
                    // 然后遍歷它 做對應的操作
                    for (URL url : failed) {
                        try {
                            // 做注冊操作
                            doRegister(url);
                            // 移除失敗集合中URL
                            failedRegistered.remove(url);
                        } catch (Throwable t) { // Ignore all the exceptions and wait for the next retry
                            logger.warn("Failed to retry register " + failed + ", waiting for again, cause: " + t.getMessage(), t);
                        }
                    }
                } catch (Throwable t) { // Ignore all the exceptions and wait for the next retry
                    logger.warn("Failed to retry register " + failed + ", waiting for again, cause: " + t.getMessage(), t);
                }
            }
        }
        ......
    }
2.1.4.6 destroy
    @Override
    public void destroy() {
        // 調用父類的方法
        super.destroy();
        try {
            // 取消執行任務
            retryFuture.cancel(true);
        } catch (Throwable t) {
            logger.warn(t.getMessage(), t);
        }
        ExecutorUtil.gracefulShutdown(retryExecutor, retryPeriod);
    }
2.1.4.7 待實現的方法
  • 這些方法都是交給不同的服務提供組件去自己實現的,后面的Zookeeper就針對這些方法做了實現。

    // ==== Template method ====

    protected abstract void doRegister(URL url);

    protected abstract void doUnregister(URL url);

    protected abstract void doSubscribe(URL url, NotifyListener listener);

    protected abstract void doUnsubscribe(URL url, NotifyListener listener);

2.2 Registry的相關Factory的實現

  • 注冊中心的工廠類,顧名思義就是生產上面的Registry的實現。
image

2.2.1 RegistryFactory

@SPI("dubbo")
public interface RegistryFactory {

    // 這個接口方法實際上就是獲取對注冊中心的連接,然后返回不同注冊中心的不同Regsitry的實現對象,
    // 注解就是根據設置不同的protocol(協議)來選擇不同的實現,
    // 比如Zookeeper,就會去使用Zookeeper的ZookeeperRegistryFactory,具體怎么選擇,后續博客再寫
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}

2.2.2 AbstractRegistryFactory

  • 類圖
image
  • 這個抽象類還是相對來說比較簡答的。咱們看一下他的類屬性

    // 注冊中心獲取過程的鎖
    private static final ReentrantLock LOCK = new ReentrantLock();

    // 注冊中心Map<注冊地址,registry> 一個類的緩存。
    private static final Map<String, Registry> REGISTRIES = new ConcurrentHashMap<String, Registry>();

2.2.2.1 getRegistryies
  • 獲取所有的registry對象
    /**
     * Get all registries
     * 獲取所有的registry對象
     * @return all registries
     */
    public static Collection<Registry> getRegistries() {
        //得到一個集合的鏡像,它的返回結果不可直接被改變,否則會報錯
        return Collections.unmodifiableCollection(REGISTRIES.values());
    }

2.2.2.2 destoryAll
  • 關閉所有已創建的registry對象
/**
     * Close all created registries
     * 關閉所有已創建的registry對象
     */
    // TODO: 2017/8/30 to move somewhere else better
    public static void destroyAll() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close all registries " + getRegistries());
        }
        //對注冊中心關閉操作加鎖
        LOCK.lock();
        try {
            // 遍歷所有的注冊中心的操作類,然后調用destroy來銷毀。
            for (Registry registry : getRegistries()) {
                try {
                    registry.destroy();
                } catch (Throwable e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
            // 然后清除集合
            REGISTRIES.clear();
        } finally {
            // Release the lock
            LOCK.unlock();
        }
    }
2.2.2.3 getRegistry
  • 獲取對應注冊中心的操作實現類
    @Override
    public Registry getRegistry(URL url) {
        // 通過URL來獲取到注冊中心的類型
        url = url.setPath(RegistryService.class.getName())
                .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
                .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
        String key = url.toServiceStringWithoutResolving();
        // 鎖定注冊中心訪問進程以確保注冊表的單個實例
        LOCK.lock();
        try {
            // 通過key來拿到對應的注冊中心的操作類
            Registry registry = REGISTRIES.get(key);
            // 有就直接返回
            if (registry != null) {
                return registry;
            }
            // 沒有就創建對應的注冊中心操作類
            registry = createRegistry(url);
            // 如果創建失敗,報錯
            if (registry == null) {
                throw new IllegalStateException("Can not create registry " + url);
            }
            // 創建成功就放到結合中
            REGISTRIES.put(key, registry);
            // 然后再返回
            return registry;
        } finally {
            // Release the lock
            LOCK.unlock();
        }
    }
2.2.2.4 createRegistry
  • 抽象方法,沒有實現,需要不同的服務提供工廠對象來自己實現對應的創建方法
    protected abstract Registry createRegistry(URL url);

2.3 Consumer And Provider InvokerWrapper

  • 實現Invoker接口,主要包裝消費者和服務提供者的屬性
  • 主要為QOS提供服務 官方地址:http://dubbo.apache.org/zh-cn/docs/user/references/qos.html
  • 什么是QOS? qos-server,是dubbo在線運維命令服務,默認端口號為:2222,用于接口命令,運維dubbo。

2.3.1 ConsumerInvokerWrapper

    // invoker對象
    private Invoker<T> invoker;
    // 原始的URL地址
    private URL originUrl;
    // 注冊中心的地址
    private URL registryUrl;
    // 消費者的地址
    private URL consumerUrl;
    // 注冊中心的Directory
    private RegistryDirectory registryDirectory;

2.3.2 ProviderInvokerWrapper

    // invoker對象
    private Invoker<T> invoker;
    // 原始的URL地址
    private URL originUrl;
    // 注冊中心的地址
    private URL registryUrl;
    // 提供者的地址
    private URL providerUrl;
    // 是否注冊
    private volatile boolean isReg;

2.4 ProviderConsumerRegTable

  • 這個類是消費者和服務提供者的注冊表操作,也是用在QOS中。
  • 主要類屬性
    // 服務提供者的Invokers集合
    public static ConcurrentHashMap<String, Set<ProviderInvokerWrapper>> providerInvokers = new ConcurrentHashMap<String, Set<ProviderInvokerWrapper>>();
    // 服務消費者的Invokers集合
    public static ConcurrentHashMap<String, Set<ConsumerInvokerWrapper>> consumerInvokers = new ConcurrentHashMap<String, Set<ConsumerInvokerWrapper>>();
  • 類圖
image
  • 里面就是一些對類屬性集合的操作,主要是QOS會用。

2.5 RegistryStatusChecker

  • 這個類就一個方法 check方法,主要是做狀態校驗。做注冊中心相關的狀態檢查校驗
  • 類上面的@Activate 注解 使這個類自動被激活加載。
    @Override
    public Status check() {
        // 獲取所有的注冊中心的對象
        Collection<Registry> registries = AbstractRegistryFactory.getRegistries();
        if (registries.isEmpty()) {
            return new Status(Status.Level.UNKNOWN);
        }
        Status.Level level = Status.Level.OK;
        StringBuilder buf = new StringBuilder();
        // 遍歷
        for (Registry registry : registries) {
            if (buf.length() > 0) {
                buf.append(",");
            }
            // 把地址拼接到一起
            buf.append(registry.getUrl().getAddress());
            // 如果注冊中心的某個節點不可用就把狀態設置成error
            if (!registry.isAvailable()) {
                level = Status.Level.ERROR;
                buf.append("(disconnected)");
            } else {
                buf.append("(connected)");
            }
        }
        // 然后返回價差的結果對象
        return new Status(level, buf.toString());
    }

2.5 RegistryDirectory and RegistryProtocol

  • 這兩個類后續再說。牽涉到其他地方的一些東西。

3. dubbo-registry-zookeeper

  • 不知道大家看到這里有沒有忘記這張圖

  • 模塊關系圖


    image
  • 所有的注冊中心實現FailbackRegistry 和 AbstractRegistryFactory來實現對應的功能。

  • 那么Zookeeper也是如此。Zookeeper主要就只有兩個類

image
    1. 是ZookeeperRegistry
    1. 是ZookeeperRegistryFactory來實現對應的功能

3.1 Dubbo在Zookeeper中的數據結構

  • dubbo在使用Zookeeper時只會創建永久節點和臨時節點。
image
  • 根節點是注冊中心分組,下面是很多的服務接口,分組來自用戶配置的<dubbo:registry>中的group屬性,默認是/dubbo。
  • 服務接口下是如圖所示的四種服務目錄,都是持久節點。
  • 服務提供者路徑/dubbo/service/providers (這里方便標識全部都用service替代接口com.demo.DemoService),下面包含接口的多個服務提供者者的URL元數據信息。
  • 服務提供者路徑/dubbo/service/consumers,下面包含接口有多個消費者的URL元數據信息
  • 服務提供者路徑/dubbo/service/routers,下面包含多個用于消費者路由策略URL元數據信息。
  • 服務提供者路徑/dubbo/service/configurators,下面包含多個用于服務提供者動態配置的URL元數據信息。

在Dubbo框架啟動時會根據我們所寫的服務相關的配置在注冊中心創建4個目錄,在providers和consumers目錄中分別存儲服務提供方、消費方元數據信息。包括:IP、端口、權重和應用名等數據。

  • 目錄包含信息
目錄名稱 存儲值樣例
/dubbo/service/providers dubbo://192.168.1.1.20880/com.demo.DemoService?key=value&...
/dubbo/service/consumers dubbo://192.168.1.1.5002/com.demo.DemoService?key=value&...
/dubbo/service/routers condition://0.0.0.0/com.demo.DemoService?category=routers&key=value&...
/dubbo/service/configurators override://0.0.0.0/com.demo.DemoService?category=configurators&key=value&...
  • 在Dubbo中啟用注冊中心:
<beans>
    <!-- 適用于Zookeeper一個集群有多個節點,多個IP和端口用逗號分隔-->
    <dubbo:registry protocol="zookeeper" address="ip:port;ip:port">
    <!-- 適用于Zookeeper多個集群有多個節點,多個IP和端口用豎線分隔-->
    <dubbo:registry protocol="zookeeper" address="ip:port|ip:port">
</beans>

3.2 ZookeeperRegistry

  • 慣例給大家一張類圖
image
  • 然后看下屬性
    // Zookeeper的默認端口號
    private final static int DEFAULT_ZOOKEEPER_PORT = 2181;

    // Dubbo在Zookeeper中注冊的默認根節點
    private final static String DEFAULT_ROOT = "dubbo";

    // 組的名稱 或者說是 根節點的值
    private final String root;

    // 服務集合
    private final Set<String> anyServices = new ConcurrentHashSet<String>();

    // zk節點的監聽器
    // Dubbo底層封裝了2套Zookeeper API,所以通過ChildListener抽象了監聽器,
    // 但是在實際調用時會通過createTargetChildListener轉為對應框架的監聽器實現
    private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<URL, ConcurrentMap<NotifyListener, ChildListener>>();

    // zk的客戶端, 對節點進行一些刪改等操作
    private final ZookeeperClient zkClient;
  • 關于Dubbo中的Zookeeper客戶端,Dubbo實現了一個統一的Client API,但是用兩種不同的Zookeeper開源庫來實現,一個是Apache的Curator,另一個是zkClient 如果用戶不設置,則默認使用Curator實現。

3.2.1 構造方法

  • 構造方法比較簡單,就是獲取組名,連接Zookeeper

    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        // 調用FailbackRegistry的構造方法
        super(url);

        if (url.isAnyHost()) {
            throw new IllegalStateException("registry address == null");
        }
        // 獲取組名稱 并復制給root
        String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
        if (!group.startsWith(Constants.PATH_SEPARATOR)) {
            group = Constants.PATH_SEPARATOR + group;
        }
        this.root = group;
        // 連接上Zookeeper
        zkClient = zookeeperTransporter.connect(url);
        // 添加連接狀態監聽器
        zkClient.addStateListener(new StateListener() {
            @Override
            public void stateChanged(int state) {
                if (state == RECONNECTED) {
                    try {
                        // 重連恢復
                        recover();
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });
    }

3.2.2 服務注冊發布與服務下線取消注冊

  • 也比較簡單就是創建節點和刪除節點
    // 發布
    @Override
    protected void doRegister(URL url) {
        try {
            zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

    // 取消發布
    @Override
    protected void doUnregister(URL url) {
        try {
            zkClient.delete(toUrlPath(url));
        } catch (Throwable e) {
            throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

3.2.3 服務訂閱和取消訂閱

  • 訂閱有pull和push兩種方式,一種是客戶端定時輪詢注冊中心拉去配置,另一種是注冊中心主動推送數據給客戶端。Dubbo目前采用的是第一次啟動拉取然后接受事件再重新拉取。
  • 再暴露服務的時候,服務端會訂閱configurators監聽動態配置,消費端啟動的時候回訂閱providers、routers、configurators類接收這三者的變更通知。
  • Dubbo在實現Zookeeper注冊中心的時候是,客戶端第一次連接獲取全量數據,然后在訂閱節點上注冊一個watcher,客戶端與注冊中心之間保持TCP長連接,后續有節點發生變化則會觸發watcher事件來把對應節點下的全量數據拉取過來。
3.2.3.1 doSubscribe
    @Override
    protected void doSubscribe(final URL url, final NotifyListener listener) {
        try {
            // 訂閱所有數據
            if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
                String root = toRootPath();
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                if (listeners == null) {
                    // 為空則把listeners放入到緩存的Map中
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                    listeners = zkListeners.get(url);
                }

                ChildListener zkListener = listeners.get(listener);
                // 創建子節點監聽器,對root下的子節點做監聽,一旦有子節點發生改變,
                // 那么就對這個節點進行訂閱.
                if (zkListener == null) {
                    // zkListener為空說明是第一次拉取,則新建一個listener
                    listeners.putIfAbsent(listener, new ChildListener() {
                        // 節點變更時,觸發通知時執行
                        @Override
                        public void childChanged(String parentPath, List<String> currentChilds) {
                            for (String child : currentChilds) {
                                // 遍歷所有節點
                                child = URL.decode(child);
                                // 如果有子節點還未被訂閱賊說明是新節點,
                                if (!anyServices.contains(child)) {
                                    // 加入到集合中
                                    anyServices.add(child);
                                    //就訂閱之
                                    subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
                                            Constants.CHECK_KEY, String.valueOf(false)), listener);
                                }
                            }
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                // 創建持久節點root,接下來訂閱持久節點的子節點
                zkClient.create(root, false);
                // 添加root節點的子節點監聽器,并返回當前的services
                List<String> services = zkClient.addChildListener(root, zkListener);
                if (services != null && !services.isEmpty()) {
                    // 遍歷所有的子節點進行訂閱
                    for (String service : services) {
                        service = URL.decode(service);
                        anyServices.add(service);
                        // 增加當前節點的訂閱,并且會返回改節點下所有子節點的列表
                        subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
                                Constants.CHECK_KEY, String.valueOf(false)), listener);
                    }
                }

                // 訂閱類別服務
            } else {
                List<URL> urls = new ArrayList<URL>();
                // 將url轉變成
                //  /dubbo/com.demo.DemoService/providers
                // /dubbo/com.demo.DemoService/configurators
                //  /dubbo/com.demo.DemoService/routers
                // 根據url類別獲取一組要訂閱的路徑 
                for (String path : toCategoriesPath(url)) {
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                    // 如果緩存沒有,則添加到緩存中
                    if (listeners == null) {
                        zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                        listeners = zkListeners.get(url);
                    }
                    ChildListener zkListener = listeners.get(listener);
                    // 同樣如果監聽器緩存中沒有 則放入緩存
                    if (zkListener == null) {
                        listeners.putIfAbsent(listener, new ChildListener() {
                            @Override
                            public void childChanged(String parentPath, List<String> currentChilds) {
                                // 通知節點變化
                                ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
                            }
                        });
                        zkListener = listeners.get(listener);
                    }
                    zkClient.create(path, false);
                    // 訂閱并返回該節點下的子路徑并緩存
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                        // 有子節點組裝,沒有那么就將消費者的協議變成empty作為url。
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
                // 回調NotifyListener,更新本地緩存信息
                notify(url, listener, urls);
            }
        } catch (Throwable e) {
            throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }
3.2.3.2 doUnsubscribe
    @Override
    protected void doUnsubscribe(URL url, NotifyListener listener) {
        // 通過url把監聽器全部拿到
        ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
        if (listeners != null) {
            ChildListener zkListener = listeners.get(listener);
            if (zkListener != null) {
                // 直接刪除group下所有的
                if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
                    String root = toRootPath();
                    // 移除監聽器
                    zkClient.removeChildListener(root, zkListener);
                } else {
                     // 移除類別服務下的監聽器
                    for (String path : toCategoriesPath(url)) {
                        zkClient.removeChildListener(path, zkListener);
                    }
                }
            }
        }
    }
3.2.3.3 其他
  • 其他代碼相對來說不是很復雜可以自行看一下。

3.3 ZookeeperRegistryFactory

  • 工廠類的代碼極其短,隨意看下。
public class ZookeeperRegistryFactory extends AbstractRegistryFactory {

    private ZookeeperTransporter zookeeperTransporter;

    public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
        this.zookeeperTransporter = zookeeperTransporter;
    }

    @Override
    public Registry createRegistry(URL url) {
        return new ZookeeperRegistry(url, zookeeperTransporter);
    }

}

3.3.1 關于ZookeeperTransporter

@SPI("curator")
public interface ZookeeperTransporter {

    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    ZookeeperClient connect(URL url);

}

  • 上面我提到過,Dubbo用Zookeeper的時候用了兩種方式實現,一個是Apache Curator,另一個是zkClient,這個類就是做看了一個轉換。如下圖


    image
  • 兩個類都實現了該接口來向外提供統一的ZookeeperClient。

  • 這個實現在remoting模塊。暫時就不講了。

4. 結語

  • 整個模塊,其他的Redis、Nacos等實現都是根據不同組件的特點來實現。功能都一樣,只是實現不一樣,大家可以自己去探索一下。
  • 整個模塊中我們單獨看的話主要是就是一個實現,一個工廠,里面牽涉到了本地緩存、重試這些機制。代碼量不是很大。認真看還是不難的。其中特別需要注意的就是注冊中心的數據結構 和 發布訂閱這些的實現了。
  • 結語有點亂。就這樣,不足之處希望留言指出,后續優化。!感謝!!!

關于我

  • 坐標杭州,普通本科在讀,計算機科學與技術專業,20年畢業,目前處于實習階段。
  • 主要做Java開發,會寫點Golang、Shell。對微服務、大數據比較感興趣,預備做這個方向。
  • 目前處于菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎大家和我交流鴨!!!
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容