# 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,

```
解压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目录下

b.编辑registry.conf文件,主要修改该文件下的registry对象和config对象内的内容。完整的配置文件在config的server目录下

###### 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.启动
分别启动每个服务,如果出现如下,则启动成功

###### 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模式)框架完成