seata 介绍与准备
seata 即分布式事务的中间件,集成在分布式的架构中,保证分布式事务的正确提交与回滚,分布式事务是分布式架构必备的功能。seata 分为服务端和客户端,基于RPC 调用实现分布式事务,而 RPC 又基于注册中心发现服务,故以下基于 nacos 作为注册中心、feign 作为 RPC 实现 seata 的搭建与集成。
— nacos 版本:1.4.3
— feign 版本跟随 spring cloud 版本
— spring cloud 版本: Hoxton.RELEASE
— seata 版本:1.4.2
seata 服务端搭建
官网文档:seata 官网。
提要说明:seata 事务存储有三种模式:file(默认)、db(数据库)、redis(缓存),三种模式优缺点在官网文档中已经说明,本文采用 db 模式部署,线上环境建议 db 高可用模式部署
# 拉取 1.4.2 的镜像文件
$ docker pull seataio/seata-server:1.4.2
# 创建容器并运行,内网环境请务必指定运行 ip 和 port,否则默认 docker 内网 ip
$ docker run --name seata-server \ -p 8091:8091 \ -e SEATA_IP=192.168.1.1 \ -e SEATA_PORT=8091 \ seataio/seata-server
# 交互模式进入
$ docker exec -it seata-server sh
# 需要修改的文件为 file.conf(存储模式)、registry.conf(注册中心与配置中心)
$ cd resources
提前在 nacos 上创建一个新的命名空间,名称自定义,如图所示:创建 seata-server.properties、seara-client.properties 两个配置文件,文件内容来源于seata 服务端与客户端配置文件,自行根据内容提示将服务端和客户端配置分类至对应的文件中,文件中存在一个分组的属性配置,开发和测试环境直接使用默认配置,线上环境如果不需要 TC 集群,也可以使用默认配置
以下为 file.conf 需要修改的部分代码截取。
注意事项:db 模式需要创建对应的数据库以及初始化对应的表(分布式事务锁),如果 mysql 版本为 8.0,需要将 mysql8.0 的驱动加入到 seata-server 的 libs 中
## store mode: file...db...redis #### 这里修改为 db
mode = "db"
## rsa decryption
public key publicKey = ""
## file store property
file {
# file 模式修改此处 ## ……
}
db {
# db 模式修改此处
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
## mysql8.0 驱动使用下面属性,5.6 使用
com.mysql.jdbc.Driver driverClassName = "com.mysql.cj.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true&serverTimezone=GMT%2B8"
user = "username"
password = "password"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
} redis {
# redis 模式修改此处 ### ……
}
以下为 registry.conf 需要修改的部分代码截取
# 注册中心配置
registry {
# file ...nacos ...eureka...redis...zk...consul...etcd3...sofa
type = "nacos"
nacos {
### seata 服务的 application-name/id
application = "seata-server"
serverAddr = "localhost:8848"
group = "SEATA_GROUP"
namespace = "nacos 对应的命名空间"
cluster = "default"
username = "nacos"
password = "nacos"
}
## 省略其他
}
# 配置中心的配置
config {
# file...nacos ...apollo...zk...consul...etcd3
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = "nacos 对应的命名空间"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
### 指定 seata-server 需要读取的配置文件ID
dataId = "seata-server.properties"
}
## 省略其他
}
修改完以上配置,重新启动 seata,启动请保证分配至少 2G 的内存空间至 seata,至此 seata 服务端部署完成,可以在刚才创建的 nacos 新空间中观察到 seata-server 的服务,如图所示:
seata 客户端配置
在任意需要seata的微服务中添加如下配置,建议全局添加,以下配置为客户端的 seata 的注册中心与配置中心,对应 seata-server 的 registry.conf 中的 registry 和 config 项。
注意事项:seata 客户端和 seata 服务端的 nacos 注册中心必须在同一个命名空间下,否则 rpc 通信无法访问,导致 xid 丢失,分布式事务失效的问题。
温馨提示:如果微服务自身也是采用 nacos 作为注册中心与配置中心,建议与 seata 的命名空间区分开,即 seata 的运行与微服务自身使用的命名空间完全独立,请勿混淆。
# 依赖引入,如果使用了 spring-cloud-alibaba 依赖管理器,请留意依赖传递版本变更问题
<seata.version>1.4.2</seata.version>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
# 全局 seata 配置,TC 集群模式下需要调整分组配置,
# 下一列对应值为 nacos 客户端配置文件 seata-client.properties 中 service.vgroupMapping.default_tx_group=default 此项属性的 key
seata.tx-service-group=default_tx_group
# seata.service.vgroup-mapping 后为上一列的 value, 对应值为 service.vgroupMapping.default_tx_group=default 的 value
seata.service.vgroup-mapping.default_tx_group=default
# seata 客户端注册中心属性配置
seata.registry.type=nacos seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=localhost:8848
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.namespace={seata-client 所在的命名空间}
seata.registry.nacos.username=nacos seata.registry.nacos.password=nacos
# seata 客户端配置中心属性配置
seata.config.type=nacos
seata.config.nacos.server-addr=localhost:8848
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.namespace={seata-client 所在的命名空间}
seata.config.nacos.data-id=seata-client.properties
seata.config.nacos.username=nacos seata.config.nacos.password=nacos
至此 seata 客户端的集成工作已完成,启动微服务使用 @GlobalTransactional(rollbackFor = Exception.class) 代替原生 @Transactional(rollbackFor = Exception.class),即可开启全局分布式事务
注:MySQL 数据库需要执行如下脚本 seata 数据库脚本
可能存在的问题
- 微服务启动后找不到 seata-server ?
- 答:seata-server 默认使用公网 ip 注册服务,如果在内网环境下部署、特别是 docker 环境下部署,seata-server 启动默认使用本地 ip,所以内网环境 docker 启动 seata 服务请务必指定 ip,保证客户端能够通过当前 ip 访问到 seata-server 即可,客户端和服务端同在一个内网环境下也可以使用内网 ip。
- 事务分组的属性值如何配置?
- 答:请参考官网文档,seata 事务分组,如果不需要 TC 集群,使用默认的配置属性即可。
- 全部按照以上步骤实现后,测试分布式事务,发生异常并没有正常回滚?
- 答:保证分布式事务使用时没有形成环式调用,即 A->B->C->A;
确保无上述情况后,保证调用深度问题,即 A->B->C、C->D、C->E-F,调用深度可能导致分布式事务失效,可以通过打印 xid 检查调用链上各服务是否属于同一个事务中,方法请参看文档seata 微服务框架支持;
seata 实现基于 rpc 框架,xid 保存在本地线程中,所以如果 rpc 采用多线程的方式访问其他服务会丢失 xid,导致全局事务失效。feign-hystrix 存在两种调用模式【信号量】【线程池】,默认使用线程池模式,请修改至信号量模式,此外 feign 调用其他服务默认不传递请求头,所以需要重写 feign 拦截器,将主服务的 xid 手动加入到下一个请求的头部中。代码示例:
- 答:保证分布式事务使用时没有形成环式调用,即 A->B->C->A;
@SpringBootConfiguration
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取请求体,同一个线程才可拿到请求体
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (ObjectUtil.isNull(request)) {
return;
}
Enumeration headerNames = request.getHeaderNames();
// 循环遍历,将上一个请求的请求头复制至下一个请求中,feign 请求是不带头部传递的,当前处理方法仅针对信号量策略生效
while (ObjectUtil.isNotNull(headerNames) && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
requestTemplate.header(headerName, request.getHeader(headerName));
}
// seata xid 传递配置, xid 保存在 LocalThread 中
String xid = RootContext.getXID();
if (StrUtil.isNotBlank(xid)) {
// 表示开启了全局事务
requestTemplate.header(RootContext.KEY_XID, xid);
}
}
}
// 或者直接引入依赖 spring-cloud-starter-alibaba-seata,官方已经重写了 open-feign 并实现了xid 的传递