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()) {
("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 = "";
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 时你肯定不这么用。另外如果没有配置这两个值,默认会使用
和 ${}-seata-service-group
这两个配置,Spring Cloud 中必须配置
,所以默认值有效,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()) {
("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
初始化配置中心
启动配置 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 不一样
以上只是客户端配置文件中需要配置的内容,seata连接nacos配置中心后,seata客户端还会读取大量的配置信息,因此其他的配置项需要在nacos中正确配置。完整的配置项参考官方的 config.txt。