seata分布式事务组件配置及服务注册到nacos整合

前言

由于近期的项目涉及到跨库的事务,之前对于分布式事务的处理用的是碧桂园旺生活平台那套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形式的配置,我觉得后期是该优化下,因为现在很多配置平台都支持拉取整份配置来解析。

nacos上的配置

好,这会就启动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;
    }
}

自定义的nacosProvider
到这边,我们的应用启动就不会再有读取不到相关配置参数的警告了。
但是日记又一直重复报出错误提示。

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可以很轻易的把这个组件用起来,但是发现当自己亲手去实践的时候,总是会有很多问题,这个过程是痛苦的,但是解决后会很有成就感,而且通过不断的跟进源码,能更清楚该框架的一些流程及他们的一些设计思想。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Spring Cloud Alibaba是Spring Cloud的一个子项目,它是阿里巴巴微服务生态的重要组成部分,提供了一系列基于Spring BootSpring Cloud的微服务组件,如服务注册与发现、配置管理、负载均衡、服务调用、熔断器等。而Nacos作为一个新兴的服务发现和配置中心,可以方便地进行服务治理。 Spring Cloud Alibaba整合Nacos的过程相对简单,只需要引入相关依赖,并在代码中使用对应的注解进行配置即可。 首先,在pom.xml文件中添加以下依赖: ```xml <!-- Nacos Discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Nacos Config --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> ``` 然后,在启动类上使用@EnableDiscoveryClient注解启用Nacos服务注册与发现功能: ```java @SpringBootApplication @EnableDiscoveryClient public class DemoApplication{ public static void main(String[] args){ SpringApplication.run(DemoApplication.class,args); } } ``` 接下来就可以使用@NacosValue注解注入配置信息: ```java @RestController public class DemoController{ @NacosValue(value="${config.key}",autoRefreshed=true) private String configValue; @GetMapping("/config") public String getConfig(){ return configValue; } } ``` 这样就可以通过Nacos Config来动态修改配置信息了。另外,Spring Cloud Alibaba还提供了一些其他有用的组件,如Sentinel、Seata等,可以方便地进行服务治理和分布式事务管理。 总的来说,Spring Cloud Alibaba整合Nacos是一个极为方便且实用的方式,它可以大大简化微服务应用的开发和部署,提高了系统的可靠性和可维护性。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值