天天看點

seata-spring-boot-starter 啟動配置

2019年看seata時版本還是0.8,再次接觸時已經1.4.2了。

曆史文章:

​​​Seata 分布式事務啟動配置分析​​​​Seata 分布式事務功能測試(一)​​​​Seata 分布式事務功能測試(二)​​​​Seata 分布式事務功能測試(三)​​

seata特殊的配置檔案形式使得入手很容易蒙,最近看官方部落格的部分文檔發現可能有不少人都有類似的感覺,最主要的原因就是 ​

​registry​

​​ 這個配置檔案名字起的不好。如果改成 ​

​bootstrap​

​ 會更容易了解。

seata支援非常多的配置和服務注冊發現方式,想要使用zookeeper,nacos等服務,首先要有一個配置知道如何去連接配接和使用這些服務。這部分的配置實際上就是 ​

​bootstrap​

​ 配置,這部分的配置非常少。

示例環境

  • 架構: Spring Cloud [Alibaba]
  • 配置和注冊中心: nacos
  • 使用 seata-spring-boot-starter [1.4.2]

用戶端最簡配置

最簡配置就是啟動必須用到的配置(包含使用預設值的),其餘的配置都需要從配置中心(​

​nacos​

​​)讀取,你在配置檔案(​

​application.[yaml|properties]​

​)配置了也無法生效。

自動配置類 - 入口配置

先看 ​

​seata-spring-boot-starter​

​ 中幾個自動配置類的注解:

@ConditionalOnProperty(prefix = SEATA_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class SeataAutoConfiguration

@ConditionalOnBean(DataSource.class)
@ConditionalOnExpression("${seata.enable:true} && ${seata.enableAutoDataSourceProxy:true} && ${seata.enable-auto-data-source-proxy:true}")
public class SeataDataSourceAutoConfiguration

@ConditionalOnProperty(prefix = SEATA_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
@ComponentScan(basePackages = "io.seata.spring.boot.autoconfigure.properties")
@AutoConfigureBefore({SeataAutoConfiguration.class, SeataDataSourceAutoConfiguration.class})
public class SeataPropertiesAutoConfiguration      

從這部分我們就已經看到了幾個配置,都是開關,而且預設都是 ​

​true​

​,可以不配置,本文為了知道用到了那些配置,是以全部記錄下來:

seata:
  enable: true # 這是個BUG,官方最新版本已經改成了 enabled,還沒釋出,想禁用就得寫全都設定false
  enabled: true
  enableAutoDataSourceProxy: true
  enable-auto-data-source-proxy: true      

在 Spring Boot 2.0 中,官方文檔中推薦使用 ​

​enable-auto-data-source-proxy​

​​ 這種烤串(用​

​-​

​​串起來)形式,他可以自動比對到駝峰和環境變量形式的名字。是以 ​

​enable-auto-data-source-proxy​

​​ 和 ​

​enableAutoDataSourceProxy​

​ 代表了相同的含義,是以這裡保留烤串,是以變成了兩個配置:

seata:
  enabled: true
  enable-auto-data-source-proxy: true      

在繼續從 seata 的入口開始,入口在 ​

​io.seata.spring.boot.autoconfigure.SeataAutoConfiguration​

​ 代碼:

@Bean
 @DependsOn({BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER, BEAN_NAME_FAILURE_HANDLER})
 @ConditionalOnMissingBean(GlobalTransactionScanner.class)
 public GlobalTransactionScanner globalTransactionScanner(
   SeataProperties seataProperties, FailureHandler failureHandler) {
     if (LOGGER.isInfoEnabled()) {
         LOGGER.info("Automatically configure Seata");
     }
     return new GlobalTransactionScanner(
            seataProperties.getApplicationId(), 
            seataProperties.getTxServiceGroup(), failureHandler);
 }      

這裡就已經看到兩個配置了 ​

​applicationId, txServiceGroup​

​​,這兩個配置在 spring cloud 中有預設值,在 spring boot 中必須手工配置。 為什麼 spring cloud 有預設值,而 spring boot 沒有?看 ​

​SeataProperties​

​ 中的代碼:

@Autowired
private SpringCloudAlibabaConfiguration springCloudAlibabaConfiguration;

public String getApplicationId() {
    if (applicationId == null) {
        applicationId = springCloudAlibabaConfiguration.getApplicationId();
    }
    return applicationId;
}

public String getTxServiceGroup() {
    if (txServiceGroup == null) {
        txServiceGroup = springCloudAlibabaConfiguration.getTxServiceGroup();
    }
    return txServiceGroup;
}      

這裡多了一層 ​

​SpringCloudAlibabaConfiguration​

​​,這個類在 ​

​Spring Boot​

​​ 使用時也存在,但是一般不會配置裡面的屬性,看​

​SpringCloudAlibabaConfiguration​

​ 中的代碼:

@Component
@ConfigurationProperties(prefix = "spring.cloud.alibaba.seata")
public class SpringCloudAlibabaConfiguration implements ApplicationContextAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(SpringCloudAlibabaConfiguration.class);
    private static final String SPRING_APPLICATION_NAME_KEY = "spring.application.name";
    private static final String DEFAULT_SPRING_CLOUD_SERVICE_GROUP_POSTFIX = "-seata-service-group";
    private String applicationId;
    private String txServiceGroup;
    private ApplicationContext applicationContext;

    /**
     * Gets application id.
     *
     * @return the application id
     */
    public String getApplicationId() {
        if (applicationId == null) {
            applicationId = applicationContext.getEnvironment()
                                .getProperty(SPRING_APPLICATION_NAME_KEY);
        }
        return applicationId;
    }

    /**
     * Gets tx service group.
     *
     * @return the tx service group
     */
    public String getTxServiceGroup() {
        if (txServiceGroup == null) {
            String applicationId = getApplicationId();
            if (applicationId == null) {
                LOGGER.warn("{} is null, please set its value", SPRING_APPLICATION_NAME_KEY);
            }
            txServiceGroup = applicationId + DEFAULT_SPRING_CLOUD_SERVICE_GROUP_POSTFIX;
        }
        return txServiceGroup;
    }      

你可以通過 ​

​spring.cloud.alibaba.seata.applicationId​

​​ 和 ​

​spring.cloud.alibaba.seata.tx-service-group​

​​ 來配置這兩個值,不用 Spring Cloud 時你肯定不這麼用。另外如果沒有配置這兩個值,預設會使用 ​

​spring.application.name​

​​ 和 ​

​${spring.application.name}-seata-service-group​

​​ 這兩個配置,Spring Cloud 中必須配置 ​

​spring.application.name​

​,是以預設值有效,Spring Boot中一般沒人配置這個,是以沒有預設值。

另外在 seata 中已經不建議使用 ​

​spring.cloud.alibaba.seata.applicationId​

​​ 和 ​

​spring.cloud.alibaba.seata.tx-service-group​

​,是以本文忽略這倆配置,直接使用優先級更高的官方推薦配置:

seata:
  application-id: 應用名
  tx-service-group:      

​GlobalTransactionScanner​

​ 初始化時會校驗上面兩個屬性必填,是以這倆是必須配置的。

在 ​

​SeataDataSourceAutoConfiguration​

​ 中的具體配置中,也有幾個存在預設值的配置:

@Bean(BEAN_NAME_SEATA_DATA_SOURCE_BEAN_POST_PROCESSOR)
 @ConditionalOnMissingBean(SeataDataSourceBeanPostProcessor.class)
 public SeataDataSourceBeanPostProcessor seataDataSourceBeanPostProcessor(SeataProperties seataProperties) {
     return new SeataDataSourceBeanPostProcessor(seataProperties.getExcludesForAutoProxying(), seataProperties.getDataSourceProxyMode());
 }

 /**
  * The bean seataAutoDataSourceProxyCreator.
  */
 @Bean(BEAN_NAME_SEATA_AUTO_DATA_SOURCE_PROXY_CREATOR)
 @ConditionalOnMissingBean(SeataAutoDataSourceProxyCreator.class)
 public SeataAutoDataSourceProxyCreator seataAutoDataSourceProxyCreator(SeataProperties seataProperties) {
     return new SeataAutoDataSourceProxyCreator(seataProperties.isUseJdkProxy(),
         seataProperties.getExcludesForAutoProxying(), seataProperties.getDataSourceProxyMode());
 }      

篩選出來就是:

seataProperties.isUseJdkProxy(),
seataProperties.getExcludesForAutoProxying(), 
seataProperties.getDataSourceProxyMode()      

預設值分别為:

  • ​true​

  • ​new String[]{}​

  • ​AT​

對應的配置為:

seata:
  use-jdk-proxy: false
  excludes-for-auto-proxying:
  data-source-proxy-mode:      

到這裡為止我們能看到所有最淺的一層配置就這幾個,其中就倆必須配置的,下面在深入到整個初始化過程中用到的所有配置。

深入初始化過程

再深入時,純靜态分析代碼已經很難找出所有配置,需要通過動态調試的方式來跟蹤出來,下面按照代碼執行順序列出所有配置。

在 ​

​GlobalTransactionScanner​

​ 初始化時,有一個字段讀取的配置:

private volatile boolean disableGlobalTransaction = ConfigurationFactory.getInstance().getBoolean(
        ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, DEFAULT_DISABLE_GLOBAL_TRANSACTION);      

這裡需要重點說一下 ​

​ConfigurationFactory​

​​,當你看到通過 ​

​ConfigurationFactory.getInstance()​

​​ 調用讀取配置時,配置是從配置中心(例如 ​

​nacos​

​​)讀取的。當你看到 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​​ 調用讀取配置時,就是從啟動配置( ​

​bootstrap​

​ )中讀取的。

是以當上面代碼要讀取 ​

​seata.service.disableGlobalTransaction​

​​ 時(預設值 ​

​false​

​​),因為要從配置中心(​

​nacos​

​​)讀取,是以就要開始初始化 ​

​nacos​

​​(其他配置中心類似)了,初始化 ​

​nacos​

​​ 配置中心時,一定會從啟動配置( ​

​bootstrap​

​​)讀取 ​

​nacos​

​ 伺服器的資訊。

​ConfigurationFactory​

​ 初始化

調用 ​

​ConfigurationFactory​

​ 方法時,首先會執行該類中的靜态方法:

static {
    load();
}

private static void load() {
    String seataConfigName = System.getProperty(SYSTEM_PROPERTY_SEATA_CONFIG_NAME);
    if (seataConfigName == null) {
        seataConfigName = System.getenv(ENV_SEATA_CONFIG_NAME);
    }
    if (seataConfigName == null) {
        seataConfigName = REGISTRY_CONF_DEFAULT;
    }
    String envValue = System.getProperty(ENV_PROPERTY_KEY);
    if (envValue == null) {
        envValue = System.getenv(ENV_SYSTEM_KEY);
    }
    Configuration configuration = (envValue == null) ? new FileConfiguration(seataConfigName,
            false) : new FileConfiguration(seataConfigName + "-" + envValue, false);
    Configuration extConfiguration = null;
    try {
        extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("load Configuration:{}", extConfiguration == null ? configuration.getClass().getSimpleName()
                    : extConfiguration.getClass().getSimpleName());
        }
    } catch (EnhancedServiceNotFoundException ignore) {

    } catch (Exception e) {
        LOGGER.error("failed to load extConfiguration:{}", e.getMessage(), e);
    }
    CURRENT_FILE_INSTANCE = extConfiguration == null ? configuration : extConfiguration;
}      

這部分是在初始化 ​

​CURRENT_FILE_INSTANCE​

​,啟動配置的初始化是一個 “雞生蛋和蛋生雞” 類似的問題,這個問題的處理需要依賴外部的環境,是以初始化中優先讀取​

​System.getProperty​

​​(對應 java 的 ​

​-Dproperty=value​

​​),不存在時再讀取 ​

​System.getenv​

​ 系統的環境變量,通過外部決定啟動配置的配置。

在 Spring [Boot|Cloud] 中使用 ​

​seata-spring-boot-starter​

​​ 內建 seata 時,根本不存在這麼一個配置檔案,在 ​

​new FileConfiguration(seataConfigName, false)​

​​ 中什麼也沒讀到,這裡最關鍵的過程在于 ​

​extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);​

​​,這裡通過 ​

​SpringBootConfigurationProvider​

​​ 動态代理 ​

​FileConfiguration​

​​,将 Spring Boot 形式的配置檔案代理了 ​

​FileConfiguration​

​ 預設的配置(細節不在展開),意思就是:

“從​

​CURRENT_FILE_INSTANCE​

​讀取配置時,你以為還在從 ​

​registry.conf​

​ 讀取配置,實際上已經從 ​

​application.[yaml|properties]​

​ 中讀取了”

是以說,初始化時,所有通過 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​​ 讀取的配置,都是我們可以在 ​

​application.[yaml|properties]​

​​ 中配置的内容。還有一個重點就是 ​

​SpringBootConfigurationProvider​

​​ 動态代理中讀取配置時,調用了 ​

​convertDataId(String rawDataId)​

​​ 方法,這個方法會給所有配置增加 ​

​seata.​

​​ 字首(還會特殊處理 ​

​.grouplist​

​​ 字尾),是以後續凡是通過 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​​ 讀取的配置,在配置檔案中配置時,手動增加 ​

​seata.​

​ 字首。

先總結一下:

  1. 通過​

    ​ConfigurationFactory.CURRENT_FILE_INSTANCE​

    ​​ 讀取的配置都在​

    ​application.[yaml|properties]​

    ​ 中配置。
  2. 通過​

    ​ConfigurationFactory.getInstance()​

    ​​ 調用讀取配置時,配置是從配置中心(例如​

    ​nacos​

    ​)讀取的。
懂 Spring Cloud的人應該知道 ​

​application.[yaml|properties]​

​ 也可以從配置中心讀取,和這裡不沖突。

​ConfigurationFactory.getInstance​

​ 初始化配置中心

啟動配置 ​

​CURRENT_FILE_INSTANCE​

​​ 初始化之後,就該 ​

​ConfigurationFactory.getInstance​

​ 初始化配置中心了。

public static Configuration getInstance() {
    if (instance == null) {
        synchronized (Configuration.class) {
            if (instance == null) {
                instance = buildConfiguration();
            }
        }
    }
    return instance;
}      

這裡是一個單例的實作,建立過程在 ​

​buildConfiguration​

​ 中,看代碼注釋:

private static Configuration buildConfiguration() {
    //注意看 CURRENT_FILE_INSTANCE,這說明是從啟動配置讀取的,也就是在 application.[yaml|properties] 中配置的
    //讀取 seata.config.type 本文配置的 nacos
    String configTypeName = CURRENT_FILE_INSTANCE.getConfig(
            ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                    + ConfigurationKeys.FILE_ROOT_TYPE);

    //忽略其他代碼,後續代碼會對 nacos 初始化
}      

在上面方法中增加了一個配置:

seata:
  config:
    type:      

上面配置 nacos 後,需要建立 nacos 對應的配置,建立過程中還要讀取很多配置:

//注意 nacos 中的這個靜态字段
private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE;
//構造方法
private NacosConfiguration() {
    if (configService == null) {
        try {
            configService = NacosFactory.createConfigService(getConfigProperties());
            initSeataConfig();
        } catch (NacosException e) {
            throw new RuntimeException(e);
        }
    }
}      

主要的配置在 ​

​getConfigProperties()​

​​,将 ​

​application.[yaml|properties]​

​​ 中的配置轉換為了一個 nacos 初始化需要用的配置檔案,這部分會讀取系統變量(​

​System.getProperty​

​​)和 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​​ 中的配置,這裡不考慮系統變量,直接列出所有 ​

​application.[yaml|properties]​

​ 中需要的配置:

seata:
  config:
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port
      namespace: #預設值空,特别注意,空使用的public,但是這裡不能寫public
      username:
      password:      

特别注意!!!

namespace 預設值空,空使用的 public,但是這裡不能寫public,如果寫了就會因為nacos的ClientWorker認為檔案和伺服器端不一緻,導緻頻繁刷日志。

連接配接 nacos 隻需要這幾個配置,隻有 ​

​server-addr​

​​ 是必填的。nacos連接配接後,通過 ​

​initSeataConfig()​

​ 初始化配置:

private static void initSeataConfig() {
      try {
          //配置中心的配置檔案 seata.config.nacos.data-id
          //預設值為 seata.properties
          String nacosDataId = getNacosDataId();
          //配置中的GROUP seata.config.nacos.group
          //預設值為 SEATA_GROUP
          String config = configService.getConfig(nacosDataId, getNacosGroup(), DEFAULT_CONFIG_TIMEOUT);
          //如果你配置中存在該配置,就會使用這個配置内容初始化 seataConfig
          //也就是說,你可以把 seata 用戶端用到的所有配置放到一個大的配置檔案中
          //如果大配置中沒有某個配置,seata 還會讀取 nacos中是否直接存在某個配置項(dataId=配置)
          if (StringUtils.isNotBlank(config)) {
              try (Reader reader = new InputStreamReader(new ByteArrayInputStream(config.getBytes()), 
                                     StandardCharsets.UTF_8)) {
                  seataConfig.load(reader);
              }
              //監控配置檔案的變化
              NacosListener nacosListener = new NacosListener(nacosDataId, null);
              configService.addListener(nacosDataId, getNacosGroup(), nacosListener);
          }
      } catch (NacosException | IOException e) {
          LOGGER.error("init config properties error", e);
      }
  }      

上面代碼在 ​

​application.[yaml|properties]​

​ 中需要的配置:

seata:
  config:
    nacos:
      data-id: seata.properties # 這是預設值
      group: SEATA_GROUP # 這是預設值      

到這裡 nacos 配置中心初始化完成了,後續擷取擷取配置時,可以從 nacos 配置中心讀取。

回到剛開始時字段初始化的代碼。

Nacos 配置中心如何配置

private volatile boolean disableGlobalTransaction = ConfigurationFactory.getInstance().getBoolean(
        ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, DEFAULT_DISABLE_GLOBAL_TRANSACTION);      

這裡擷取配置檔案的方式就是讀取 nacos 配置中心的内容,預設值為 ​

​false​

​。nacos 配置中心有兩種配置該配置的方式。

先看代碼中讀取配置的部分:

@Override
public String getLatestConfig(String dataId, String defaultValue, long timeoutMills) {
    //先讀取系統屬性System.getProperty
    String value = getConfigFromSysPro(dataId);
    if (value != null) {
        return value;
    }
    //這裡的seataConfig是Properties,從nacos讀取的seata.properties,上面代碼有這個初始化過程
    //這裡的seata.properties算是大配置,裡面可以配置所有屬性
    value = seataConfig.getProperty(dataId);
    //如果大配置沒有
    if (null == value) {
        try {
            //直接從nacos讀取配置
            value = configService.getConfig(dataId, getNacosGroup(), timeoutMills);
        } catch (NacosException exx) {
            LOGGER.error(exx.getErrMsg());
        }
    }

    return value == null ? defaultValue : value;
}      

從代碼可以看出有三種來源,按配置優先級順序如下:

  1. 系統屬性,通過​

    ​-Dkey=val​

    ​ 配置
  2. 從seataConfig讀取,在 nacos 的 seata.properties 中配置
  3. 直接從 nacos 讀取

第1點不考慮,先看第2點,截個圖友善了解:

seata-spring-boot-starter 啟動配置

配置的内容:

seata-spring-boot-starter 啟動配置

再看第3種,第3種可能是官方推薦的方式,因為官方針對 nacos 提供了 shell 和 py 腳本來導入配置資訊,導入資訊的格式就是第3種:

seata-spring-boot-starter 啟動配置

通過腳本導入到nacos的配置如下:

seata-spring-boot-starter 啟動配置

以上隻是 nacos 配置中心相關的配置,下面繼續看注冊中心。

注冊中心相關配置

注冊中心的初始化在 ​

​RegistryFactory.getInstance()​

​ 中:

public static RegistryService getInstance() {
     if (instance == null) {
         synchronized (RegistryFactory.class) {
             if (instance == null) {
                 instance = buildRegistryService();
             }
         }
     }
     return instance;
 }

 private static RegistryService buildRegistryService() {
     RegistryType registryType;
     String registryTypeName = ConfigurationFactory.CURRENT_FILE_INSTANCE.getConfig(
         ConfigurationKeys.FILE_ROOT_REGISTRY + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
             + ConfigurationKeys.FILE_ROOT_TYPE);
     try {
         registryType = RegistryType.getType(registryTypeName);
     } catch (Exception exx) {
         throw new NotSupportYetException("not support registry type: " + registryTypeName);
     }
     if (RegistryType.File == registryType) {
         return FileRegistryServiceImpl.getInstance();
     } else {
         return EnhancedServiceLoader.load(RegistryProvider.class, Objects.requireNonNull(registryType).name()).provide();
     }
 }      

仍然是個單例,在初始化的時候,從 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​​ 讀取了 ​

​seata.registry.type​

​​,這裡以 ​

​nacos​

​ 為例。

和配置一樣,需要讀取連接配接 nacos 的基本資訊,這裡和配置需要的參數一樣,隻是改成了 registry的配置,初始化過程中的所有配置如下:

seata:
  registry:
    type: nacos
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port
      namespace:
      username:
      password:      

在目前類中搜尋所有使用 ​

​ConfigurationFactory.CURRENT_FILE_INSTANCE​

​ 的代碼,發現還有下面幾個配置:

seata:
  registry:
    nacos:
      cluster: default
      application: seata-server
      group: DEFAULT_GROUP #預設值和 config 的 SEATA_GROUP 不一樣      

總結

通過以上分析,當我們使用 seata-spring-boot-starter,配置和注冊中心使用 nacos 時,​

​application.yaml​

​ 配置檔案中需要配置的項非常少,必須配置的内容如下:

seata:
  application-id: 應用名 #Spring Cloud可選,Spring Boot必填
  tx-service-group: 事務分組名 #Spring Cloud可選,Spring Boot必填
  #配置中心
  config:
    type: nacos #必填
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port
  #服務注冊發現
  registry:
    type: nacos #必填
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port      
seata:
  enable: true # 這是個BUG,官方最新版本已經改成了 enabled,還沒釋出,想禁用就得寫全,都設定false
  enabled: true #可選
  enable-auto-data-source-proxy: true #可選
  use-jdk-proxy: false #可選
  excludes-for-auto-proxying:  #可選
  data-source-proxy-mode: AT  #可選
  application-id: 應用名 #Spring Cloud可選,Spring Boot必填
  tx-service-group: 事務分組名 #Spring Cloud可選,Spring Boot必填
  #配置中心
  config:
    type: nacos #必填
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port
      namespace: #可選,預設值空
      username:  #可選
      password:  #可選
      data-id: seata.properties # 這是預設值
      group: SEATA_GROUP # 這是預設值
  #服務注冊發現
  registry:
    type: nacos #必填
    nacos:
      server-addr: IP:port #預設http,如果是https一定要配置為 https://HOSTNAME:port
      namespace: #可選,預設值空
      username:  #可選
      password:  #可選
      cluster: default #可選
      application: seata-server #可選
      group: DEFAULT_GROUP #預設值和 config 的 SEATA_GROUP 不一樣