暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

seata-spring-boot-starter 启动配置

MyBatis 2021-06-11
1685

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: AT

到这里为止我们能看到所有最浅的一层配置就这几个,其中就俩必须配置的,下面在深入到整个初始化过程中用到的所有配置。


深入初始化过程

再深入时,纯静态分析代码已经很难找出所有配置,需要通过动态调试的方式来跟踪出来,下面按照代码执行顺序列出所有配置。

在 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 对应的配置,创建过程中还要读取很多配置:

//注意 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点,截个图方便理解:

配置的内容:

再看第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。


文章转载自MyBatis,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论