# cloud-eureka-seata **Repository Path**: ly-springcloud/cloud-eureka-seata ## Basic Information - **Project Name**: cloud-eureka-seata - **Description**: SpringCloud 使用Eureka作为注册中心,加入阿里巴巴分布式事务Seata组件 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2021-11-01 - **Last Updated**: 2022-12-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringCloud ## README ##### 一. 框架构成     本教程,采用SpringCloud Hoxton.SR8版 为分布式底层框架,持久层使用Mybatis-Plus,注册中心为Eureka,各个服务间的通信使用的是openfeign,使用阿里巴巴Seata作为分布式事务,seata版本为1.4.2。本教程采用分布式的默认AT模式。 ##### 二. 注意事项     pom文件中千万不要用以下配置,网上很多教程都是这样使用,虽然也能正常,但是控制台会出现无限循环错误,使用1.4.2时会报Could not found property service. disableGlobalTransaction的错误,且无限循环,使用1.3.0时控制台会无限打印错误日志seata fileListener execute error:null,虽然两者都有错误,但是能程序依然正常。解决方案中在file.conf文件中添加disableGlobalTransaction = false 依然无法解决。[这里有错误讨论](https://github.com/seata/seata/issues/2114),以下是错误配置,也是网上教程目前最多的配置 ``` io.seata seata-all 1.4.2 com.alibaba.cloud spring-cloud-alibaba-seata 2.2.0.RELEASE seata-all io.seata ``` 正确的引入依赖方式如下(通过之前搭建的seata zk给予的灵感) ``` com.alibaba.cloud spring-cloud-alibaba-seata 2.2.0.RELEASE io.seata seata-all io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter 1.4.2 ``` ##### 三. 环境准备 ###### 1.启动Eureka服务 启动cloud-eureka-server程序即可,配置等信息这里不做过多说明,主要就是引入eureka服务依赖 ``` org.springframework.cloud spring-cloud-starter-netflix-eureka-server 启动入口类加上@EnableEurekaServer注解 以及yml中配置如下 #服务端口号 server: port: 8200 --- #服务名称 spring: application: name: cloud-eureka-server --- #eureka配置 eureka: instance: #注册中心地址 hostname: localhost #客户端调用地址 client: service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #是否将自己注册到Eureka服务中,因为该应用本身就是注册中心,不需要再注册自己(集群的时候为true) registerWithEureka: false #是否从Eureka中获取注册信息,因为自己为注册中心,不会在该应用中的检索服务信息 fetchRegistry: false ``` ###### 2.下载seata,[下载地址](https://github.com/seata/seata/releases),同样选择的是当时最新的版本1.4.2, ![输入图片说明](https://images.gitee.com/uploads/images/2021/0917/103757_5ca6b289_1198099.png "微信截图_20210914111144.png") ``` 解压seata-server到目录下,例如D:\Program Files\seata-server-1.4.2 进入到conf目录下,修改file.conf.example 为file.conf,并编辑file.conf文件和registry.conf文件 首先编辑file.conf,因为seata服务需要有数据库支撑,因此我们需要新建一个数据库,数据库名(自定义),但是要保证和配置文件内保持一致。 我这里使用seata作为数据库名 ``` a.编辑file.conf文件,并新建seata数据库,数据库表在sql目录下 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0917/112959_4ee89825_1198099.png "微信截图_20210917112940.png") b.编辑registry.conf文件,主要修改该文件下的registry对象和config对象内的内容。完整的配置文件在config的server目录下 ![输入图片说明](https://images.gitee.com/uploads/images/2021/1101/154418_b694cb92_1198099.png "屏幕截图.png") ###### 3.初始化seata服务的数据库 ``` 此3个表是seata服务的默认表结构和目录,可在github的seata仓库内找到,我这里直接放到了sql目录下,可以至二级导入到数据库 DROP TABLE IF EXISTS `branch_table`; CREATE TABLE `branch_table` ( `branch_id` bigint(20) NOT NULL, `xid` varchar(128) NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `resource_group_id` varchar(32) DEFAULT NULL, `resource_id` varchar(256) DEFAULT NULL, `lock_key` varchar(128) DEFAULT NULL, `branch_type` varchar(8) DEFAULT NULL, `status` tinyint(4) DEFAULT NULL, `client_id` varchar(64) DEFAULT NULL, `application_data` varchar(2000) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `global_table`; CREATE TABLE `global_table` ( `xid` varchar(128) NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(64) DEFAULT NULL, `transaction_service_group` varchar(64) DEFAULT NULL, `transaction_name` varchar(64) DEFAULT NULL, `timeout` int(11) DEFAULT NULL, `begin_time` bigint(20) DEFAULT NULL, `application_data` varchar(2000) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`,`status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `lock_table`; CREATE TABLE `lock_table` ( `row_key` varchar(128) NOT NULL, `xid` varchar(96) DEFAULT NULL, `transaction_id` mediumtext, `branch_id` mediumtext, `resource_id` varchar(256) DEFAULT NULL, `table_name` varchar(32) DEFAULT NULL, `pk` varchar(32) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`row_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ###### 4.启动seata服务 ``` 进入到D:\Program Files\seata-server-1.4.2的bin目录下,双击seata-server.bat 启动seata服务 ``` ##### 四. 后端框架服务准备 ###### 1.分布式事务seata的配置 ``` com.alibaba.cloud spring-cloud-alibaba-seata 2.2.0.RELEASE io.seata seata-all io.seata seata-spring-boot-starter io.seata seata-spring-boot-starter 1.4.2 seata: enabled: true # seata的id,可以不用配置,默认会读取程序配置的application.name application-id: ${spring.application.name} # my_test_tx_group为自定义配置,必须和vgroup-mapping:下的配置保持一致 tx-service-group: my_test_tx_group service: vgroup-mapping: #与上诉tx-service-group的值保持一致,seata-eureka为注册到Eureka服务的服务名,是配置在registry.conf中的application内容 my_test_tx_group: seata-eureka grouplist: #seata默认的地址 default: 127.0.0.1:8091 enable-auto-data-source-proxy: true config: type: file file: name: file.conf registry: type: eureka eureka: #seata注册到eureka的服务名 application: seata-eureka #eureka服务地址 serviceUrl: http://localhost:8200/eureka/ weight: 1 ``` ##### 五. Mybatis-Plus 的配置 因为seata需要代理才能真正的使用,[因此查看了github的seata分支](https://github.com/seata/seata-samples/blob/master/springcloud-eureka-feign-mybatis-seata),但都是mybatis的,我这里使用的是mybatis-plus,所以只能进行改造。启动类的@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)排除调自动配置,使用配置文件走代理 ``` @Configuration public class DruidConfig { /** * 初始化druid 数据库 */ @Bean(name = "druidDataSource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } /** * init datasource proxy * * @Param: druidDataSource datasource bean instance * @Return: DataSourceProxy datasource proxy */ @Primary @Bean("dataSource") public DataSourceProxy dataSource(DataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } } ``` ##### 六. 为每个服务都初始化 undo_log 数据表 ``` 比如我这里有三个服务程序对应三个不同的库: 订单服务:cloud-seata-order 仓储服务:cloud-seata-storage 为每个服务的数据库都创建undo_log表 DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; ``` ##### 七. 模拟实际应用 模拟一个普通的订单库存交易流程,用户创建订单后扣减库存,在service层加上@GlobalTransactional注解即可,如下是部分代码,完整代码可去 cloud-seata-order程序中查看,这里服务间的通讯使用的是feign ``` @GlobalTransactional(rollbackFor = Exception.class) public void create(OrderCreateDto createDto) { log.info("---------->开始交易"); this.save(createDto); storageFeignService.decreaseStorage(new DecreaseStorageDto(createDto.getProductId(),createDto.getCount())); log.info("---------->交易结束"); ``` ##### 八. 启动及注意事项 ###### 1.启动 分别启动每个服务,如果出现如下,则启动成功 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0917/170610_3cbe3786_1198099.png "微信截图_20210910142900.png") ###### 2.注意事项 分别模拟该流程成功和失败的情况,观察数据库是否回滚。发现正常情况下肯定没问题,但是失败的情况下,只有当前程序回滚,而其他服务没有回滚,这完全不正确,理应是各个服务都回滚才对。虽然理解seata服务时通过xid保证全局唯一的,但是找不到解决的办法,能排除的是程序没问题,而是服务间的调用出了问题,查阅资料才发现因为使用了feign各个服务间没有传递xid导致seata通讯异常([这里用到了feign但是没有提到解决办法,在issues下会看到有人提问该问题](https://github.com/seata/seata-samples/blob/master/springcloud-eureka-feign-mybatis-seata)),解决办法是,在各个服务传递间加入请求头xid,并使用拦截器绑定各个服务传递的xid 实现RequestInterceptor 在请求头内加入xid ``` /** * @author 蚂蚁会花呗 * @date 2021/9/9 20:39 */ @Component public class FeignHeaderInterceptor implements RequestInterceptor { // 这里在feign请求的header中加入xid // 注意:这里一定要将feign.hystrix.enabled设为false,因为为true时feign是通过线程池调用,而XID并不是一个InheritablThreadLocal变量。 @Override public void apply(RequestTemplate requestTemplate) { String xid = RootContext.getXID(); if (StringUtils.isNotBlank(xid)) { requestTemplate.header("xid", xid); } } } ``` 拦截器内接收xid并进行绑定 ``` /** * @author 蚂蚁会花呗 * @date 2021/9/10 14:11 */ @Component public class SeataXidFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String restXid = request.getHeader("xid"); if (StringUtils.isNotBlank(restXid)) { RootContext.bind(restXid); } filterChain.doFilter(request, response); } } ``` 经过以上配置后,重新启动各个服务,并模拟失败的情况,发现能正常回滚了,至此,SpringCloud、Eureka、Mybati-Plus、Seata(AT模式)框架完成