十七、微服务之Seata

十七、微服务之Seata

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 数据库脚本

可能存在的问题
  1. 微服务启动后找不到 seata-server ?
    • 答:seata-server 默认使用公网 ip 注册服务,如果在内网环境下部署、特别是 docker 环境下部署,seata-server 启动默认使用本地 ip,所以内网环境 docker 启动 seata 服务请务必指定 ip,保证客户端能够通过当前 ip 访问到 seata-server 即可,客户端和服务端同在一个内网环境下也可以使用内网 ip。
  2. 事务分组的属性值如何配置?
    • 答:请参考官网文档,seata 事务分组,如果不需要 TC 集群,使用默认的配置属性即可。
  3. 全部按照以上步骤实现后,测试分布式事务,发生异常并没有正常回滚?
    • 答:保证分布式事务使用时没有形成环式调用,即 A->B->C->A;
      确保无上述情况后,保证调用深度问题,即 A->B->C、C->D、C->E-F,调用深度可能导致分布式事务失效,可以通过打印 xid 检查调用链上各服务是否属于同一个事务中,方法请参看文档seata 微服务框架支持
      seata 实现基于 rpc 框架,xid 保存在本地线程中,所以如果 rpc 采用多线程的方式访问其他服务会丢失 xid,导致全局事务失效。feign-hystrix 存在两种调用模式【信号量】【线程池】,默认使用线程池模式,请修改至信号量模式,此外 feign 调用其他服务默认不传递请求头,所以需要重写 feign 拦截器,将主服务的 xid 手动加入到下一个请求的头部中。代码示例:
@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 的传递