分布式事务Seata-1.5.2使用全路线指北

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

本文给大家带来的是Seata最新版的尝鲜体验,会详细描述我遇到的坑、详配置过程、使用方法等等。也会聊聊我对分布式事务的理解,顺道给大家推荐一些我看过的比较好的文章。

分布式事务大家应该都不陌生,应该也用过不少好用的方法来实现,比如Seata、手写事务补偿性代码、本地消息表。我一开始做技术选型的时候,选的是Seata,当时的版本是1.4.X,主要是看重AT模式,使用够简单,侵入性不低但是能够接受。在做部署和本地化的时候感觉挺顺畅,就是官方文档当时写的有点模糊,很多关键的点都没有明确给出解决方案,比如详细配置、XID传递。

上周分布式事务同事使用的时候突然又出现问题了,我就想借着这个口子把Seata升级到1.5.2,十几天前刚刚更新的,正好尝尝鲜。新特性的话,没有特别关注细节,看到有个控制台,但是本质上还是Seata服务端的单表查询,感觉意义不大,期待官方能有更多新功能。

环境需求及部署情况

  • seata1.5.2--集成方式spring-boot-starter
  • nacos2.0以上--注册及配置中心
  • 扩展OpenFeign植入XID,请求链路绑定XID

部署的方式很淳朴,单节点部署在服务器上,分了正式和测试,分别对接公司的正式和测试的nacos三节点集群,高可用肯定是没有了,不过一年过去没出啥问题倒是真的幸运。Seata服务端配置中心和注册中心都是用的nacos,项目集成用的spring-boot-starter,因为没用Alibaba-Cloud,所以自己得做XID的绑定和传递。使用的还是AT模式,性能比较好,而且集成简单。

Server端配置

文件解压

seata.io/zh-cn/blog/…

下载1.5.2安装包,上传至服务器解压

tar -xvf /wa/seata-server-1.5.2.tar.gz

修改配置文件

修改/conf/application.yml

cd /wa/seata/conf

修改/conf/application.yml文件,修改nacos相关配置

server:
  port: 7091
spring:
  application:
    name: seata-new-uat
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  # 没有删掉下面扩展
#  extend:
#    logstash-appender:
#      destination: 127.0.0.1:4560
#    kafka-appender:
#      bootstrap-servers: 127.0.0.1:9092
#      topic: logback_to_logstash
# 控制台用户名密码
console:
  user:
    username: seata
    password: wzy
seata:
  config:
    type: nacos
    nacos:
      server-addr: xxx
      namespace: xxx
      group: SEATA_GROUP
      username: xxx
      password: xxx
      data-id: seataNew
  registry:
    type: nacos
    nacos:
      server-addr: xxx
      namespace: xxx
      group: SEATA_GROUP
      username: xxx
      password: xxx
      application: seata-new-uat
      cluster: default
  # 后面server或者client配置都不用配,因为配置了nacos作为配置中心。security得配,不配报错
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
复制代码

关联Nacos配置中心

进入nacos,创建命名空间

image.png 进入配置列表,进入seata命名空间,新建配置

image.png

dataId为seataNew,group为SEATA_GROUP,类型为properties

image.png

配置详情参考seata.io/zh-cn/docs/…

源码里的全部配置,会比官网上的更多。链接github.com/seata/seata…

实际使用,修改如下,完成后发布(参数没有详细说明的都是默认配置)

#公共部分
transport.serialization=seata
transport.compressor=none
transport.heartbeat=true
registry.type=nacos
config.type=nacos
#server端
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
#存储模式我这里选用的db,不用file和redis,这里很多配置都没有默认值但是必须指定,按情况配置即可
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
#这里url跟上rewriteBatchedStatements=true,原因看官网-参数配置-附录7,简单来说就是增加批量插入效率
store.db.url=jdbc:mysql://xxx:3306/seata_new?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&rewriteBatchedStatements=true
store.db.user=xxx
store.db.password=xxx
#默认120,稍微调大点
store.db.minConn=5
store.db.maxConn=30
store.db.maxWait=5000
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table
store.db.queryLimit=100
#监控,只支持prometheus,我这里没有现成的就没用
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
#client端
seata.enabled=true
seata.enableAutoDataSourceProxy=true
seata.useJdkProxy=false
transport.enableClientBatchSendRequest=true
client.log.exceptionRate=100
service.disableGlobalTransaction=false
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.rm.reportSuccessEnable=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
#一阶段全局提交和回滚结果上报TC重试次数,默认1,这里改成3
client.tm.commitRetryCount=3
client.tm.rollbackRetryCount=3
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.logTable=undo_log
client.undo.onlyCareUpdateColumns=true
client.rm.sqlParserType=druid
#自定义事务组scm_tx_group和my_test_tx_group
service.vgroupMapping.scm_tx_group=default
复制代码

Seata资源准备

官方文档 seata.io/zh-cn/docs/…

在配置中心里指定的地方,新建数据库seata_new,utf8mb4,运行下面sql。(这个是给seata服务端用来做全局事务管理的)

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
复制代码

所有使用seata服务的项目数据库,都运行下面sql,增加一张表。(这个是在每一个事务参与者和事务发起者的数据库里增加一张表,用于本地事务回滚)

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
复制代码

运行Seata服务端

返回服务器,运行shell文件,指定注册IP为服务器IP,端口号默认8091

错误操作

sh /wa/seata/seata-1.5.2/bin/seata-server.sh -h 172.16.3.248 -p 8091
复制代码

启动异常

Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication

github.com/seata/seata…

已经有人提过了,脚本问题

正确操作--到脚本所在位置执行脚本

cd /wa/seata/seata-1.5.2/bin
sh seata-server.sh -h 172.16.3.248 -p 8091
复制代码

如下图则成功,不成功检查报错,一般是nacos未配置 image.png

成功启动后注册至nacos

image.png

Client端配置

环境准备

官方Tips:请确保client与server的注册处于同一个namespace和group,不然会找不到服务---不一定,我使用的时候就不在一起

引入依赖,引入seata和nacos,nacos这里随意

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>${nacos-client.verison}</version>
</dependency>
复制代码

请求植入XID

spring-cloud-starter-alibaba-seata默认会传递XID,而seata-spring-boot-starter和seata-all不会,需要手动实现,官方文档这里有写

image.png

改造feign的请求拦截器,用于植入Seata用xid。同理其他的调用方式改造对应的拦截器即可

import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.seata.core.context.RootContext;
import org.springframework.context.annotation.Configuration;

/**
 * @Classname FeignInterceptor
 * @Date 2021/9/26 22:18
 * @Author WangZY
 * @Description openfeign植入seata用xid
 */
@Configuration
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        String xid = RootContext.getXID();
        requestTemplate.header(RootContext.KEY_XID, xid);
    }
}
复制代码

绑定XID

XID的传递,这里有两种方式都可以实现,一种是基于过滤器,一种是基于拦截器。思路就是请求过来的时候绑定XID,请求结束后解绑XID。Filter没有案例,按照我这个写就行。

过滤器

import io.seata.core.context.RootContext;
import org.springframework.util.StringUtils;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @Classname SeataFilter
 * @Date 2021/9/27 11:38
 * @Author WangZY
 * @Description Seata过滤器
 */
@WebFilter
public class SeataFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String xid = req.getHeader(RootContext.KEY_XID.toLowerCase());
        boolean isBind = false;
        if (!StringUtils.isEmpty(xid)) {
            RootContext.bind(xid);
            isBind = true;
        }
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            if (isBind) {
                RootContext.unbind();
            }
        }
    }
}
复制代码

拦截器

拦截器的案例,我们可以在alibaba-cloud包里直接拿过来用,引入这个依赖源码拉下来直接粘。

dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.1</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
复制代码

image.png

封装组件

这里我们可以顺道做一个组件,方便集成,例如我这里做了一个feign-seata的组件。结构很简单,我这里为了测试,过滤器和拦截器都放里面了,实际放一个即可,feign拦截器就是上面的代码,SeataAutoConfiguration.class就是一个扫描包的类,我之前的教你如何开发一个spring-boot-starter里面有提到这个作用。

image.png

项目配置

application.properties配置文件引入

替换对应的事务组

seata.tx-service-group=XXX

seata.service.vgroup-mapping.XXX=default

seata.enabled=true
seata.enable-auto-data-source-proxy=true
seata.tx-service-group=替换事务组
seata.registry.type=nacos
# 和nacos上面的服务名一致
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=172.16.3.85:8847,172.16.3.86:8847,172.16.3.87:8847
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.namespace=32d19a70-085d-430d-b2b3-7e8d06b37308
seata.registry.nacos.cluster=default
seata.registry.nacos.username=nacos
seata.registry.nacos.password=RJ0p-0p-0p-
seata.service.vgroup-mapping.替换事务组=default
复制代码

使用方法

@GlobalTransactional(rollbackFor = Exception.class,timeoutMills=60000)

全局事务默认只有60秒,长事务必须拆分,或者抽出无关逻辑,如果实在无法拆分,建议timeout参数调大

image.png

Seata注意事项

seata.io/zh-cn/docs/…

1.无法找到集群"defalut"

常见于各种网络问题、加密密钥问题导致nacos没有正常连接。 以下仅作为我在查询各种网络连接问题时的断点位置

io.seata.discovery.registry.nacos.NacosRegistryServiceImpl#getNamingProperties
io.seata.discovery.registry.nacos.NacosRegistryServiceImpl#lookup
com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo
com.alibaba.nacos.client.naming.net.NamingProxy#queryList
com.alibaba.nacos.client.naming.net.NamingProxy#callServer(java.lang.String, java.util.Map, java.util.Map, java.lang.String, java.lang.String)
复制代码

2.全局事务失效

问题表现为服务A是事务发起方,服务B是事务参与者,然后发起全局事务后报错回滚,最后结果是服务A回滚,B没有回滚,Seata服务端日志显示回滚成功。

解决思路:

首先是查看Seata的日志,发现服务A和B都正常注册成功,并且流程中发现Seata服务端日志打印了回滚成功。

开始排查配置,最终发现是feign依赖没有采用我修改后的版本,核心原因是XID没有传递成功。

3.nginx会抹掉header中的下划线

因为seata中XID传递时使用TX_XID为key进行传递,因此事务参与者接收nginx转发处理后的请求会找不到key为TX_XID的header,导致出现和问题2一样的全局事务失效问题。解决方法是修改nginx配置。

在nginx里的nginx.conf配置文件中的http部分中添加如下配置(默认off)
underscores_in_headers on;
复制代码

聊聊分布式事务

Seata in AT mode

Seata 的 AT 模式建立在关系型数据库的本地事务特性的基础之上,通过数据源代理类拦截并解析数据库执行的 SQL,记录自定义的回滚日志,如需回滚,则重放这些自定义的回滚日志即可。AT 模式虽然是根据 XA 事务模型(2PC)演进而来的,但是 AT 打破了 XA 协议的阻塞性制约,在一致性和性能上取得了平衡。

AT 模式是基于 XA 事务模型演进而来的,它的整体机制也是一个改进版本的两阶段提交协议。AT 模式的两个基本阶段是:

  • 第一阶段:首先获取本地锁,执行本地事务,业务数据操作和记录回滚日志在同一个本地事务中提交,最后释放本地锁;
  • 第二阶段:如需全局提交,异步删除回滚日志即可,这个过程很快就能完成。如需要回滚,则通过第一阶段的回滚日志进行反向补偿。

各种事务模式的选择

我个人在选择的时候比较看好AT和TCC。先来说说AT,相对于别的事务模式优势很明显,非阻塞和简单易用。TCC的话更多的是可操作范围大,更自由。其他诸如本地消息表、消息队列、Saga或多或少相对麻烦或者有限制。这里推荐几篇好文,顺道说说我看完后的碎碎念。

juejin.cn/post/684490…

掘金一千赞的文章,不仅仅是少见而且是真的牛逼。从基础到原理,辅以各种类型解决方案,如果你只想看理论,认准这篇就没错了。稍显不足的是,缺了点实践部分,用来做入门了解肯定是绰绰有余,但是真要上手进行实践,肯定是不行的。

juejin.cn/post/684490…

看了几十篇,感觉大家理论这块确实整不出花活了,都是你抄抄我抄抄。这一篇算是比较全的,图也比较多,算是比较清晰的。

写在最后

网上看了好多,感觉分布式事务这块成熟的中间件好少,大多都是推荐用Seata的,别的就是自定义的居多。有RocketMq、TCC,总觉得这块不应该是这样,我个人觉得分布式事务在现在的开发环境中应该算是必备了吧,优先级应该比日志收集这种观测性的更高才对,竞品太少真的太意外了。

目前使用Seata应该算是轻度,没有接SkyWalking之类的监控,也没有做高可用,其实风险蛮高的。但是项目时间太紧,摆烂,不然这些应该去做的。分布式事务属于业务场景复杂的中间件了,通用的方案我现在一般就是用Seata的AT。除此之外对接外部系统的时候情况最麻烦,用过本地消息表的思想去处理,确保数据的最终一致性。

本文以实践为主,算是一篇使用说明书,本来想写成那种成知识体系的文章,但是鉴于我对分布式事务的了解尚浅,就不误人子弟了。

猜你喜欢

转载自juejin.im/post/7129686175710707720