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
調用
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.
字首。
先總結一下:
- 通過
讀取的配置都在ConfigurationFactory.CURRENT_FILE_INSTANCE
中配置。application.[yaml|properties]
- 通過
調用讀取配置時,配置是從配置中心(例如ConfigurationFactory.getInstance()
)讀取的。nacos
懂 Spring Cloud的人應該知道 application.[yaml|properties]
也可以從配置中心讀取,和這裡不沖突。
ConfigurationFactory.getInstance
初始化配置中心
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;
}
從代碼可以看出有三種來源,按配置優先級順序如下:
- 系統屬性,通過
配置-Dkey=val
- 從seataConfig讀取,在 nacos 的 seata.properties 中配置
- 直接從 nacos 讀取
第1點不考慮,先看第2點,截個圖友善了解:
配置的内容:
再看第3種,第3種可能是官方推薦的方式,因為官方針對 nacos 提供了 shell 和 py 腳本來導入配置資訊,導入資訊的格式就是第3種:
通過腳本導入到nacos的配置如下:
以上隻是 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 不一樣