Seata 之 config 模塊源碼解讀

本文首發於個人微信公衆號《andyqian》,關注即可獲取一線互聯網內推機會!

前言

我們在編寫項目過程中,配置文件幾乎是標配,從最簡單且最常用的 application.properties 文件,到現在集羣環境下的配置中心。也算是互聯網技術發展的一個縮影(單體應用到分佈式應用)。在這方面,目前有非常多的開源實現,如:zookpeer,nacos,apollo 等等。像是命題作文下交付的不同答卷,各有優劣,當然,這有點偏題了。回到正題,今天討論的話題是: Seata 中 config 模塊的實現,主要會從以下幾個方面進行分析:

  1. 職責。
  2. 設計
  3. 源碼
  4. 設計模式

職責

config 模塊承擔的主要職責是:seata 框架配置文件的生命週期管理。這句話怎麼理解呢?我們都知道,seata 框架的定位就是 分佈式事務框架,從廣義上來說也是中間件,而中間件就涉及到諸如:網絡傳輸 (如:netty),傳輸就涉及到協議(如自定義的seata協議,http協議 …), 編解碼 等等。恰恰對於中間件而言,在這方面應該予以用戶更多的選擇,而用戶的個性化選擇則在配置文件或配置中心體現。

設計

上面談到了中間件應該予以用戶更多的選擇。在這方面:Microkernel + Plugin 模式是一個優秀的設計,其在 Dubbo 中有着非常好的實踐。同樣的:在 Seata 中,也有相同的設計,config 模塊就是一個非常好的例子:

module 圖

其中:

  1. seata-config-core 就是配置核心 module,值得注意的是:seata-config-all 是 config 模塊一個父pom,而並不是一個Plugin。
  2. 其他的 module 則以 Plugin 形式存在,目前有:apollo, consul,zk,nacos 等等。

類圖

  1. Configuration 接口是 config 模塊一個非常重要的接口,所有配置相關的操作都圍繞着該接口設計。不同的註冊中心實現該接口,或者繼承該接口的抽象類 AbstractConfiguration 類即可。其職責是:定義配置信息的原子操作,如:get,put, remove 等。類圖如下所示:

 

 

 

需要注意的是: FileConfiguration 是 Seata 默認的配置實現。

除了Configuration 接口外,還有一個重要的接口就是:ConfigurationProvider,則表示 Configuration 能力的提供。類圖如下所示:

 

 

入口

在 config 模塊中,對外提供的入口是:ConfigurationFactory 類,通過其靜態方法:getInstance() 對外提供 Configuration 接口的能力。

源碼

通過上面的設計分析,現在我們來看看源碼。

1. 首先,先看 Configuration 接口的源碼:

public interface Configuration<T> {

 /**
  * Gets duration.
  * @param dataId       the data id
  * @param defaultValue the default value
  * @param timeoutMills the timeout mills
  * @return he duration
  */
 Duration getDuration(String dataId, Duration defaultValue, long timeoutMills);

/**
  * Gets config.
  * @param dataId       the data id
  * @param defaultValue the default value
  * @param timeoutMills the timeout mills
  * @return the config
  */
 String getConfig(String dataId, String defaultValue, long timeoutMills);

  /**
  * Put config boolean.
  * @param dataId       the data id
  * @param content      the content
  * @param timeoutMills the timeout mills
  * @return the boolean
  */
 boolean putConfig(String dataId, String content, long timeoutMills);

 /**
  * Put config if absent boolean.
  * @param dataId       the data id
  * @param content      the content
  * @param timeoutMills the timeout mills
  * @return the boolean
  */
 boolean putConfigIfAbsent(String dataId, String content, long timeoutMills);

  /**
  * Remove config listener.
  * @param dataId   the data id
  * @param listener the listener
  */
 void removeConfigListener(String dataId, T listener);

 /**
  * Gets config listeners.
  * @param dataId the data id
  * @return the config listeners
  */
 List<T> getConfigListeners(String dataId);

 /**
  * Gets config from sys pro.
  * @param dataId the data id
  * @return the config from sys pro
  */
 default String getConfigFromSysPro(String dataId) {
     return System.getProperty(dataId);
 }

...

備註:上述源碼經過一些簡單的篩選,實際還有很多重載函數,其底層調用的還是上述方法,故省略。

2. ConfigurationProvider 類的源碼則非常簡單,實例化對應的 ConfigurationProvider 即可。以 Zookeeper 爲例,源碼如下所示:

@LoadLevel(name = "ZK", order = 1)
public class ZookeeperConfigurationProvider implements ConfigurationProvider {
 @Override
 public Configuration provide() {
     try {
         return new ZookeeperConfiguration();
     } catch (Exception e) {
         throw new RuntimeException(e);
     }
 }

3. AbstractConfiguration 抽象類是 Configuration 接口的默認實現。除了 Configuration 定義的接口默認實現外,還定義了一個抽象方法:

public abstract class AbstractConfiguration<T> implements Configuration<T> {

    /**
     * The constant DEFAULT_CONFIG_TIMEOUT.
     */
    protected static final long DEFAULT_CONFIG_TIMEOUT = 5 * 1000;

    ... // 因篇幅原因,不在這裏顯示

     /**
     * Gets type name.
     *
     * @return the type name
     */
    public abstract String getTypeName();

4. ConfigurationFactory 類是使用 config 模塊的入口,同時也是聚合,源碼如下所示:

public final class ConfigurationFactory {
    private static final String REGISTRY_CONF_PREFIX = "registry";
    private static final String REGISTRY_CONF_SUFFIX = ".conf";
    private static final String ENV_SYSTEM_KEY = "SEATA_ENV";
    private static final String ENV_PROPERTY_KEY = "seataEnv";
    /**
     * the name of env
     */
    private static String envValue;

    // 加載配置文件
    static {
        envValue = System.getProperty(ENV_PROPERTY_KEY);
        if (null == envValue) {
            envValue = System.getenv(ENV_SYSTEM_KEY);
        }
    }

    //定義默認的 file configuration instant,
    private static final Configuration DEFAULT_FILE_INSTANCE = new FileConfiguration(
        REGISTRY_CONF_PREFIX + REGISTRY_CONF_SUFFIX);
    public static final Configuration CURRENT_FILE_INSTANCE = null == envValue ? DEFAULT_FILE_INSTANCE : new FileConfiguration(REGISTRY_CONF_PREFIX + "-" + envValue
        + REGISTRY_CONF_SUFFIX);
    private static final String NAME_KEY = "name";
    private static final String FILE_TYPE = "file";

    private static volatile Configuration instance = null;

    /**
     * // 單例獲取 Configuration
     * @return the instance
     */
    public static Configuration getInstance() {
        if (instance == null) {
            synchronized (Configuration.class) {
                if (instance == null) {
                    instance = buildConfiguration();
                }
            }
        }
        return instance;
    }

    // 構造 Configuration 對象
    private static Configuration buildConfiguration() {
        ConfigType configType = null;
        String configTypeName = null;
        try {
            configTypeName = CURRENT_FILE_INSTANCE.getConfig(
                ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                    + ConfigurationKeys.FILE_ROOT_TYPE);
            configType = ConfigType.getType(configTypeName);
        } catch (Exception e) {
            throw new NotSupportYetException("not support register type: " + configTypeName, e);
        }
        //如果 configType = file,則構造 FileConfiguration 實例
        if (ConfigType.File == configType) {
            String pathDataId = ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                + FILE_TYPE + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                + NAME_KEY;
            String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
            return new FileConfiguration(name);
        } else {
           // 否則通過 spi 進行加載service,並調用其 provide方法,例如:zookpeer, naocs , 等等。return EnhancedServiceLoader.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name())
                .provide();
        }
    }

5. FileConfiguration 爲 Seata 的默認 Configuration, 其底層依賴 typesafe.config 開源框架實現。源碼如下所示:

public class FileConfiguration extends AbstractConfiguration<ConfigChangeListener> {

     private static final Logger LOGGER = LoggerFactory.getLogger(FileConfiguration.class);

    private final Config fileConfig;

    private ExecutorService configOperateExecutor;

    private ExecutorService configChangeExecutor;

    ....

      public FileConfiguration(String name) {
        if (null == name) {
            fileConfig = ConfigFactory.load();
        } else {
            //加載指定名稱的 configName
            fileConfig = ConfigFactory.load(name);
        }
        // 構造配置獲取的線程池
        configOperateExecutor = new ThreadPoolExecutor(CORE_CONFIG_OPERATE_THREAD, MAX_CONFIG_OPERATE_THREAD,
            Integer.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
            new NamedThreadFactory("configOperate", MAX_CONFIG_OPERATE_THREAD));
        // 構造配置改變線程池,並啓動, 每 1秒進行獲取一次。configChangeExecutor = new ThreadPoolExecutor(CORE_CONFIG_CHANGE_THREAD, CORE_CONFIG_CHANGE_THREAD,
            Integer.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
            new NamedThreadFactory("configChange", CORE_CONFIG_CHANGE_THREAD));
        configChangeExecutor.submit(new ConfigChangeRunnable());
    }
}

由於篇幅原因,config 模塊的源碼沒有詳細的列出,不過核心的源碼如上所示。對於zookpeer, naocs 等的代碼,則是按照 seata 定義的接口進行實現,可自行查看。

設計模式

在 config 模塊中,使用到了多個設計模式,其中包括:

  1. ConfigurationFactory 類 中的 getInstance() 爲典型的 單例模式
  2. Configuration 接口, AbstractConfiguration 抽象類,以及其子類 爲典型的 適配器模式

config 模塊的代碼相對而言,還算容易理清楚。不過裏面還涉及到一些例如:SPI 的設計,還沒有分析,下次這個會單獨摘成一篇文章呈現出來。如果你對源碼也非常感興趣,歡迎進羣我們一起研讀。


 

相關閱讀:

接口設計的五點建議 !

Seata 分佈式事務框架

Seata 之 rm-datasource 源碼解讀

Dubbo 線程池源碼解析

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章