seata-spring-boot-starter 启动配置
  ehrZuhofWJiC 2天前 3 0

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.​​ 前缀。

先总结一下:

  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 不一样

以上只是客户端配置文件中需要配置的内容,seata连接nacos配置中心后,seata客户端还会读取大量的配置信息,因此其他的配置项需要在nacos中正确配置。完整的配置项参考官方的 ​​config.txt​​。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2天前 0

暂无评论