前言
由于近期的项目涉及到跨库的事务,之前对于分布式事务的处理用的是碧桂园旺生活平台那套TCC;但是对于两阶段提交的处理方式,happylifeTcc的代码侵入性太高了。需要自己去写业务的提交跟回滚逻辑。
所以决定采用呼声很高的seata。对于seata的介绍及底层实现网上的文章还是比较多的,这里不做介绍;同时,对于nacos服务的搭建这里也不做介绍,主要是把整合seata的服务注册及配置到nacos上做介绍以及遇到的一些坑,网上也有相关文章,但是在把配置注册到nacos上很多网上的方案并不可用,其中缺少了很重要的一个配置,下面会讲。所以决定记录下本次整合的过程,希望对你们有帮助。
对于为什么使用nacos而不要zk,redis等,是因为我们项目上目前是使用nacos作为配置管理及服务治理框架,有兴趣大家自己去尝试吧。
那为什么不用默认的file去做配置呢,主要是为了后期的seata服务做集群高可用考虑;其实如果服务器环境是用k8s搭建的话,基于k8s的svc,用file模式理论上也是可以达到高可用,因为svc你也可以把它当作一个服务治理的插件来使用,有兴趣的话请自行研究。
版本说明
seata服务我这边是从 seata镜像 拉取的docker镜像去搭建;
这里说下,服务跟应用上的版本匹配很重要,1.0.0之前跟之后的版本都是有区别的。这个后面会提到,如果大家也是要以镜像去搭建seata服务的话,注意拉取时带上tag,如果直接拉取的是latest,相应的服务依赖的版本也应该是最新的,我这边服务跟客户端用的都是1.0.0的版本。
项目中引入如下依赖,该依赖下的seata核心包是1.0.0的版本。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
seata服务搭建
从docker的镜像详情页上,已经有介绍如何启动一个seata服务了,根据需要修改相应的参数,我们在seata-server/resources目录下新增我们的注册文件registry.conf,配置如下,将服务注册及配置类型都改为nacos,然后修改nacos的相关参数为你们自己的参数。
registry {
type = "nacos"
nacos {
serverAddr = "nacos"
namespace = ""
}
}
config {
type = "nacos"
nacos {
serverAddr = "nacos"
namespace = ""
}
}
但在启动seata服务之前,我们需要先把相关的配置先推送到nacos上,从 seata服务源码 载下代码,找到该目录seata-1.0.0\script\config-center下config.txt文件,我这边是指定了seata-server的事务日志存储方式为db(需要建相关的库表,sql文件在seata-1.0.0\script\server\db),所以需要把store相关的一些数据库连接参数配置下;
还有就是 service.vgroup_mapping.my_test_tx_group=default
服务组这个配置,如果有多个 服务组的话就配置多个,需要跟你应用的 application.yml 中的 seata.tx-service-group.my_test_tx_group
一致,不然会导致应用启动时报错 no available service 'null' found, please make sure registry config correct
;在这里就涉及到不同版本这个配置不同了,1.0.0及之前的版本,像上面那样配置没错,但是从1.1.0开始,vgroup_mapping 变成 vgroupMapping,这是需要注意的;这个可以从启动加载配置的时候跟进源码去查看。
配置好config.txt后,将该文件拷贝到seata-server目录下,同时从源码的seata-1.0.0\script\config-center\nacos 目录下nacos-config.sh文件也拷贝进来,执行该sh脚本(带上你nacos的地址),即可在nacos的配置列表看到有Group是SEATA_GROUP的相关配置了,如下图config中有多少条配置,这就会有多少行,但是这样的展现形式我觉得很不友好,为何不整在一个配置项里面呢?从github上的issues中看到,其实也有人有提过这样的问题了,他们给的说法是为了兼容其他配置平台所以才做成单条K-V形式的配置,我觉得后期是该优化下,因为现在很多配置平台都支持拉取整份配置来解析。
好,这会就启动seata容器,如果日记没报错且最后是[main]io.seata.core.rpc.netty.AbstractRpcRemotingServer.start:155 -Server started ...
表明seata服务启动成功,我们可以在nacos的服务列表看到一个服务名为 serverAddr 的服务。
客户端配置
数据源代理
我们知道,对于事务的处理,最重要的是要拿到数据源,因为通过数据源我们可以控制事务什么时候回滚或提交,所以数据源我们需要让seata来代理,在我们的启动注解上排除自动加载的数据源@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
并配置seata数据源代理
@Configuration
public class DataSourceConfiguration {
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
// 以下几行如果不加的话,会导致mybatis插件的分页失效
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
sqlParserList.add(new BlockAttackSqlParser());
paginationInterceptor.setSqlParserList(sqlParserList);
bean.setPlugins(new Interceptor[] {paginationInterceptor, new OptimisticLockerInterceptor()});
//
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath:com/**/mapper/*Mapper.xml"));
SqlSessionFactory factory;
try {
factory = bean.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
return factory;
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
resources配置
- 将seata服务上的那份 registry.conf 直接拷贝过来放在应用的 resources 目录下,
- application.yml 中要配置服务组,前面已经说了。
undo_log表
在有涉及分布式事务的数据库中创建undo_log表,用于保存需要回滚的数据。
脚本在 seata-1.0.0\script\client\at\db 目录下
问题处理
至此,看到网上有些方案就说已经完成,只需添加@GlobalTransactional
注解就能实现分布式事务。确实这会应用启动正常,不会报错,但是会有如下相关参数找不到的警告;说明我们在nacos上的配置信息并没有被读取到,但是我们明明nacos上有添加相关参数了啊
2020-04-22 09:33:17.343 WARN 13980 --- [nfigOperate_2_2] io.seata.config.FileConfiguration : Could not found property transport.thread-factory.boss-thread-prefix, try to use default value instead.
2020-04-22 09:33:17.344 WARN 13980 --- [nfigOperate_2_2] io.seata.config.FileConfiguration : Could not found property transport.thread-factory.worker-thread-prefix, try to use default value instead.
2020-04-22 09:33:17.344 WARN 13980 --- [nfigOperate_2_2] io.seata.config.FileConfiguration : Could not found property transport.thread-factory.share-boss-worker, try to use default value instead.
2020-04-22 09:33:17.350 WARN 13980 --- [nfigOperate_2_2] io.seata.config.FileConfiguration : Could not found property transport.thread-factory.worker-thread-size, try to use default value instead.
跟进源码查看,发现seata在启动的时候是通过 ConfigurationFactory 去加载配置,该类中有如下静态代码块
static {
String seataConfigName = System.getProperty(SYSTEM_PROPERTY_SEATA_CONFIG_NAME);
if (null == seataConfigName) {
seataConfigName = System.getenv(ENV_SEATA_CONFIG_NAME);
}
if (null == seataConfigName) {
seataConfigName = REGISTRY_CONF_PREFIX;
}
String envValue = System.getProperty(ENV_PROPERTY_KEY);
if (null == envValue) {
envValue = System.getenv(ENV_SYSTEM_KEY);
}
Configuration configuration = (null == envValue) ? new FileConfiguration(seataConfigName + REGISTRY_CONF_SUFFIX,
false) : new FileConfiguration(seataConfigName + "-" + envValue + REGISTRY_CONF_SUFFIX, false);
Configuration extConfiguration = null;
try {
//这段代码用的是SPI机制去加载我们自定义的扩展配置类
extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("load extConfiguration:{}",
extConfiguration == null ? null : extConfiguration.getClass().getSimpleName());
}
} catch (Exception e) {
LOGGER.warn("failed to load extConfiguration:{}", e.getMessage(), e);
}
CURRENT_FILE_INSTANCE = null == extConfiguration ? configuration : extConfiguration;
}
前面主要是根据是否有配置注册文件及环境参数来读取registry文件,我们注意到它会通过EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
(java的SPI机制)去加载我们自定义的配置类,在EnhancedServiceLoader类中会去读取META-INF/services/
或 META-INF/seata/
目录下的文件
所以,我们需要去自定义一个nacos的配置类,实现 ExtConfigurationProvider 接口,然后在 META-INF/services/
目录下新建一个文件名为io.seata.config.ExtConfigurationProvider
内容为 我们自定义的 NacosConfigurationProvider 的全路径名的文件,注意类上面的注解@LoadLevel
,因为在EnhancedServiceLoader中会去判断该配置是什么类型的配置,取到我们在registry中配置的与name匹配的配置,以及如果配置有多份,则根据order顺序取最优先的那份配置,然后通过反射去实例化该类。
我看到好几篇对于seata整合nacos的文章都没有对这点有做说明,是因为他们跟我版本不一致吗?我看到0.7.1的版本,也是用这种SPI的加载方式去配置我们自定义的配置啊,还是说没有注意到启动时未读取到配置信息的警告?
@LoadLevel(name = "Nacos", order = 1)
public class NacosConfigurationProvider implements ExtConfigurationProvider {
public NacosConfigurationProvider(){}
@Override
public Configuration provide(Configuration configuration) {
return configuration;
}
}
到这边,我们的应用启动就不会再有读取不到相关配置参数的警告了。
但是日记又一直重复报出错误提示。
2020-04-23 13:29:33.326 ERROR 2468 --- [imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct
2020-04-23 13:29:33.528 ERROR 2468 --- [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager : no available service 'default' found, please make sure registry config correct
跟进 NettyClientChannelManager 源码查看,客户端与seata服务通讯使用的是Netty,连接不上的时候一直在执行reconnect方法
说明是因为获取不到可用的服务节点数据,继续跟进去,我们会发现getAvailServerList方法会去获取我们的注册实例,我们用的是nacos,然后通过我们registry中的nacos连接信息去创建一个nacos的NamingInstance实例,然后调用
List<Instance> firstAllInstances = getNamingInstance().getAllInstances("serverAddr", clusters);
获取服务名为 serverAddr 的所有服务数据,继续跟进代码,发现最后确实有调用了nacos提供获取服务列表的api:http://192.168.***.***:8848/nacos/v1/ns/instance/list,但是返回的数据中,最关键的hosts,连接参数信息为空,为什么呢?
{
"metadata": {},
"dom": "serverAddr ",
"cacheMillis": 3000,
"useSpecifiedURL": false,
"hosts": [],
"name": "DEFAULT_GROUP@@serverAddr",
"checksum": "44b6889b7e2783de4f121d",
"lastRefTime": 1587882060789,
"env": "",
"clusters": "default"
}
最后我通过postman,模拟他源码里面的请求,确实hosts是个空集合,在这里卡了一个早上,后面注意到我们传进去的clusters参数,代表是集群的意思,我传进去的是小写的default,但是我nacos上serverAddr服务的节点集群名称是大写的DEFAULT!!!
在postman上把clusters参数换成大写,果然
{
"metadata": {},
"dom": "serverAddr",
"cacheMillis": 3000,
"useSpecifiedURL": false,
"hosts": [
{
"valid": true,
"marked": false,
"metadata": {},
"instanceId": "*.*.*.*#8091#DEFAULT#DEFAULT_GROUP@@serverAddr",
"port": 8091,
"healthy": true,
"ip": "*.*.*.*",
"clusterName": "DEFAULT",
"weight": 1.0,
"ephemeral": true,
"serviceName": "serverAddr",
"enabled": true
}
],
"name": "DEFAULT_GROUP@@serverAddr",
"checksum": "139653a888853fa72163bc",
"lastRefTime": 1587882417192,
"env": "",
"clusters": ""
}
那在调用方法时
List<Instance> firstAllInstances = getNamingInstance().getAllInstances("serverAddr", clusters);
这个clusters参数是从哪里传过来的呢?发现是我们配置在nacos上的那个服务组service.vgroup_mapping.my_test_tx_group=default
这边我们配置的是default,但是这份配置我们的从seata源码中拷贝过来的啊,而且我在网上看到的也全部都是这样配置,并没用用大写DEFAULT。难道是我nacos上有参数没有配置好还是哪里需要忽略大小写吗?这点还没有去验证。
最后把nacos上的服务组改成大写DEFAULT后,启动正常,调用相关业务,正常。
总结
本以为根据seata官方提供的demo可以很轻易的把这个组件用起来,但是发现当自己亲手去实践的时候,总是会有很多问题,这个过程是痛苦的,但是解决后会很有成就感,而且通过不断的跟进源码,能更清楚该框架的一些流程及他们的一些设计思想。