最详细实际项目中seata踩坑、部署教程以及多链路调用

最详细实际项目中seata踩坑、部署教程以及多链路调用

情景说明

项目上有涉及账务订单的模块需要添加分布式事务,了解到seata的AT模式代码侵入量比较小,所以研究了一下,但是跟着网上各种教程资料走了一遍发现还是会有不少的坑,特此分享一下踩坑记录。

环境

由于项目组已经有了现成的一套k8s开发环境,这里我准备直接在本地windows下部署seata,然后在原工程结构上添加seata,也是为了后续更好地在项目上集成。

部署

安装部署seata版本1.4.2下载地址
下载后目录应该如下
seata1.4.2目录结构

这时候我们需要修改config目录里面两个文件file.conf和registry.conf
file.conf修改点如下,主要是自定义的事务组,修改db模式以及数据库连接信息,这边我是新建了一个数据库seata,里面需要添加三张表,建表语句如果压缩包里面没有可以上官网找,我这里直接贴出来

file.config注意修改点

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_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

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;

CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

registry.conf修改点主要也是模式启用nacos,然后加上自己的nacos连接信息,其中注意group和namespace要与你的nacos一致,因为seata的配置项太多了,我这里新建了一个group单独放seata的配置项,下面一步就是将seata配置信息导入到nacos config配置中心

在这里插入图片描述

config.txt和nacos-config.sh修改点,config.txt就是seata各种详细的配置,执行 nacos-config.sh 即可将这些配置导入到nacos,这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。
笔者下载的1.4.2版本conf文件夹内没有config.txt和nacos-config.sh文件,所以要单独下载放入,下面有获取地址
nacos-config.sh下载地址
config.txt下载地址

在这里插入图片描述

config.txt修改项这里完整贴出来,注意中文注释需要去掉,否则运行可能会报错,我这里是为了解释说明

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=true
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3

# 这里是nacos的配置组,非常重要!

service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=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
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
store.mode=file
store.lock.mode=file
store.session.mode=file
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

# 以下是db连接信息,需要注意 

store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://172.16.4.25:30726/seata?useUnicode=true&characterEncoding=utf8&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true
store.db.user=seata
store.db.password=seata123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1

# 这里是回滚重试时间,上面rollbackingRetryPeriod是间隔时间,我这里设置重试3秒,防止数据库出现脏写无限重试

server.maxRollbackRetryTimeout=3s
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

config.txt改好了之后就可以将配置项导入nacos了,我这里直接用的git bash执行nacos-config.sh,在git bash界面输入:

sh nacos-config.sh -h 172.16.4.27 -p 30090 -g local_dev_seata -t d51d2105-3a11-4f34-a7ed-f841ec5c2174 -u nacos -w nacos

在这里插入图片描述

注:命令解析:-h -p 指定nacos的端口地址;-g 指定配置的分组,注意,是配置的分组;-t 指定命名空间id; -u -w指定nacos的用户名和密码,同样,这里开启了nacos注册和配置认证的才需要指定。
当然也可以直接修改nacos-config.sh脚本,成功后nacos配置项如图:

在这里插入图片描述

配置导入到nacos以后就可以直接启动seata了,找到bin目录下的seata-server.bat,直接管理员身份运行即可

在这里插入图片描述

成功启动可以在nacos服务列表中看到我们的seata服务

在这里插入图片描述

实战(多链路调用)

微服务集成seata,这里我用到四个服务,与前台交互的BFF服务,订单服务,库存服务,账务服务,为了验证在实际项目上的运用,我这里直接拿了项目组已有的bff模块以及其他三个中心服务模块,在三个业务库中分别新建了三张表t_order,t_storage,t_account,代表订单表,库存表和账务表,注意想要实现数据回滚,需要在对应的三个业务库中添加日志回滚表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`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

seata的代码侵入:在我们部署好seata服务,创建好业务库后,下一步就是代码层面了,相信准备使用或者想要使用seata的朋友对seata已经有所了解,本人对源码也没有研究太深就不班门弄斧了,这里主要介绍seata AT模式相关的代码配置与依赖,为什么是AT模式?毕竟免费而且代码侵入量很少

  • pom
    相关服务需要添加seata依赖:
    	<!-- seata-->
    	<dependency>
    		<groupId>io.seata</groupId>
    		<artifactId>seata-all</artifactId>
    		<version>1.0.0</version>
    	</dependency>
    	<dependency>
    		<groupId>io.seata</groupId>
    		<artifactId>seata-spring-boot-starter</artifactId>
    		<version>1.0.0</version>
    	</dependency>
    	<dependency>
    		<groupId>com.alibaba.cloud</groupId>
    		<artifactId>spring-cloud-alibaba-seata</artifactId>
    		<version>2.2.0.RELEASE</version>
    	</dependency>
    	<dependency>
    		<groupId>com.alibaba</groupId>
    		<artifactId>druid-spring-boot-starter</artifactId>
    		<version>1.1.10</version>
    	</dependency>
    	<dependency>
    		<groupId>com.alibaba</groupId>
    		<artifactId>druid</artifactId>
    		<version>1.1.10</version>
    	</dependency>
    
  • Application
    bff端启动类添加数据源代理,禁止 SpringBoot 自动注入数据源配置。
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
    
  • yml
    相关服务的yml中添加seata连接信息
    spring:
      cloud:
        refresh:
          refreshable: none
        alibaba:
          seata:
            enabled: true
            enable-auto-data-source-proxy: true
            tx-service-group: my_test_tx_group
            registry:
              type: nacos
              nacos:
                application: seata-server
                server-addr: http://172.16.4.27:30090
                username: nacos
                password: nacos
            config:
              type: nacos
              nacos:
                server-addr: http://172.16.4.27:30090
                group: local_dev_seata
                username: nacos
                password: nacos
                namespace: bda53f33-65b6-401b-8f95-d2338f079d4d
            service:
              vgroup-mapping:
                my_test_tx_group: default
              disable_global_transaction: false
            client:
              rm:
                report-success-enable: false
    

目前项目组持久层框架用的是jpa,三个业务库的单表增删改查代码这里就不赘述了,bff调用各个业务中心的代码这里贴一下:

@GlobalTransactional(rollbackFor = Exception.class)
    public void create(TOrderTrans order) throws Exception {
//        String xid = GlobalTransactionContext.getCurrentOrCreate().getXid();
//        order.setXid(xid);
        TOrderTrans tOrderTrans = order;
        // 1 新建订单
        logger.info("----->开始新建订单");
        String url1 = centerProperties.getGatewayServiceUrl() + centerProperties.getOrderLogicServiceUrl()
                + "/tOrder/createOrder";
        ResponseEntity<JsonData> tOrderTransResponse =
                restTemplateUtils.post(url1, JSON.toJSON(order), JsonData.class);
        if (tOrderTransResponse == null || !JsonEnum.SUCCESS.code().equals(tOrderTransResponse.getBody().getCode())) {
            logger.debug("新建订单失败,失败原因:{}", tOrderTransResponse.getBody().getMsg());
        } else {
            tOrderTrans = JSON.parseObject(JSON.toJSONString(tOrderTransResponse.getBody().getData()), TOrderTrans.class);
        }

        // 2 扣减库存
        logger.info("----->订单微服务开始调用库存,做扣减Count");
        String url2 = centerProperties.getGatewayServiceUrl() + centerProperties.getFinanceLogicServiceUrl()
                + "/tStorage/decrease";
        TStorageTrans tStorageTrans = new TStorageTrans();
        tStorageTrans.setId(order.getProductId());
        tStorageTrans.setResidue(order.getCount());
        restTemplateUtils.post(url2, JSON.toJSON(tStorageTrans), JsonData.class);
        logger.info("----->订单微服务开始调用库存,做扣减End");

        // 3 扣减账户
        logger.info("----->订单微服务开始调用账户,做扣减Money");
        String url3 = centerProperties.getGatewayServiceUrl() + centerProperties.getAccountsLogicServiceUrl()
                + "/tAccount/decrease";
        TAccountTrans tAccountTrans = new TAccountTrans();
        tAccountTrans.setId(order.getProductId());
        tAccountTrans.setResidue(order.getCount());
        restTemplateUtils.post(url3, JSON.toJSON(tAccountTrans), JsonData.class);
        logger.info("----->订单微服务开始调用账户,做扣减End");

        // 4 修改订单状态,从0到1,1代表已完成
        logger.info("----->修改订单状态开始");
        String url4 = centerProperties.getGatewayServiceUrl() + centerProperties.getOrderLogicServiceUrl()
                + "/tOrder/updateStatus";
        tOrderTrans.setStatus("1");
        restTemplateUtils.post(url4, JSON.toJSON(tOrderTrans), JsonData.class);

        logger.info("----->下订单结束");
        throw new Exception("测试失败");

    }

抛开业务代码不谈,重点其实就是 @GlobalTransactional 注解,在bff服务中service方法加上这个注解其实就是开启全局事务。

再说一下业务逻辑,这里测试的是下订单->减库存->扣余额->改订单状态的场景,因为笔者这里的业务中心服务又分逻辑服务和数据服务(对应logic-service和db-service),这就导致服务调用可能是多链路的,以上流程正常应该是

BFF
order
storage
account

这种业务场景其实代码写到这里已经可以实现分布式事务回滚了,bff调用订单、库存以及账务服务时,以订单为例,首先业务表插入一条数据:

在这里插入图片描述

同时订单库的undo_log表中会记录一条快照信息,分别是订单表变更前(before images)和变更后(after image)的快照,事务结束正常就会释放,异常就会根据快照回滚,我这里是打了个断点来截取快照信息。

在这里插入图片描述

实际上由于业务中心逻辑层数据层的分割,调用链可能会是

BFF
order-logic
order-db
storage-logic
storage-db
account-logic
account-db

甚至于可能是这种

BFF
order-logic
storage-logic
order-db
storage-db
account-logic
account-db

针对这些多链路调用服务的情况,笔者亲测除了BFF层发生异常事务会回滚,其他logic或者db服务即使发生异常影响的也只是它自己的业务数据,其他模块数据依旧,另外在logic层多次调用logic以及db服务时,如果其中一次调用失败,其他调用的数据也不会回滚,这就违背了我们一开始在BFF层service方法加全局事务的初衷。其实这就是全局事务,事务管理器TM(Transaction Manager)需要在全局事务外层捕获全局异常,所以调用链内部的异常需要抛到最外层统一处理。总结起来就是A->B->C->…服务调用,虽然我们的GlobalTransactional全局事务注解只是加在A的service方法上,但是在BC服务中的异常我们需要主动抛出来,然后在A的service判断BC等服务是否成功调用,不成功则手动throw,注意,如果使用try catch的话,需要在catch块中手动添加代码回滚。

	GlobalTransactionContext.reload(RootContext.getXID()).rollback();

踩坑总结

  • 启动报错 这边添加完代码启动服务会有报错,原因是工程中已经存在一个叫做serviceProperties的bean,是项目上用来配置各个服务地址的,而seata的jar包里也存在一个重名的io.seata.spring.boot.autoconfigure.properties.file.ServiceProperties,这里我们直接将自己工程里面的serviceProperties重命名就好了。
	Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'serviceProperties' for bean class [io.seata.spring.boot.autoconfigure.properties.file.ServiceProperties] conflicts with existing, non-compatible bean definition of same name and class [com.XXX.center.accounts.logic.constant.ServiceProperties]
  • db数据未回滚

    第一种情况是多链路调用链内部的异常需要抛到最外层统一处理,或者异常被catch住了,这种修改一下业务层异常处理逻辑或者添加代码手动回滚即可

    第二种情况就是配置问题了,公司内部的组件如果实现了WebMvcConfigurer接口或者继承了WebMvcConfigurationSupport会导致XID无法正常传递,就会导致分支事务无法注册,分支事务就会不受TC管理。
    在这里插入图片描述

/**
	 * 添加SeataHandlerInterceptor拦截器
	 */
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new SeataHandlerInterceptor()).addPathPatterns(new String[]{"/**"});
	}

以我的工程为例,公司logic服务存在一个JsonConfig实现了WebMvcConfigurer接口,导致全局事务回滚时,logic层数据会不会滚,解决方法也很简单,参考com.alibaba.cloud.seata.web.SeataHandlerInterceptorConfiguration#addInterceptors将xid传递拦截器加入即可

在这里插入图片描述
在这里插入图片描述
由于笔者持久层框架用的是jpa,也是遇到了另一种情况:逻辑层直接调用jpa的通用的数据访问控制层(如下图方式2),这种也无法回滚,但是这种调用的本质还是请求db的微服务,然后调用jpa通用的数据访问控制层(AbstractRepositoryRestController),那就在db的aop层去传递xid即可。
在这里插入图片描述
在这里插入图片描述
解决方案:添加切面前置通知 @Before (前置通知, 在方法执行之前执行),手动绑定xid
在这里插入图片描述

	@Before(value = "log()")
    public void before(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String xid = RootContext.getXID();
            String rpcXid = request.getHeader("TX_XID");
//            if (logger.isDebugEnabled()) {
//                logger.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
//            }
            if (xid == null && rpcXid != null) {
                RootContext.bind(rpcXid);
//                if (logger.isDebugEnabled()) {
//                    logger.debug("bind {} to RootContext", rpcXid);
//                }
            }
        }
    }

至此,seata的部署、启动、集成以及多链路调用各种情景测试回滚完毕。

### Seata 1.5.2 版本安装与部署分步指南 #### 准备工作 在开始安装和部署 Seata 1.5.2 前,需确保环境已准备好 Docker 和 Docker Compose 工具。此外,还需确认 Nacos 注册中心已经通过 Docker 正确部署并运行正常[^3]。 #### 使用 Docker 部署 Nacos Server 创建 `docker-compose.yml` 文件用于定义 Nacos 的服务配置: ```yaml version: '3' services: nacos: image: nacos/nacos-server:2.1.0 container_name: nacos ports: - "8848:8848" environment: - MODE=standalone restart: always ``` 执行以下命令启动 Nacos 服务: ```bash docker-compose up -d ``` 此操作会拉取 Nacos 容器镜像并以后台模式运行容器。 #### 使用 Docker 部署 Seata Server 同样利用 Docker Compose 创建 Seata Server 的服务配置文件: ```yaml version: '3' services: seata-server: image: seataio/seata-server:v1.5.2 container_name: seata-server ports: - "8091:8091" environment: - SERVICE_VGROUP_MAPPING=my_test_tx_group=default - STORE_MODE=file volumes: - ./registry.conf:/seata/config/registry.conf - ./file_store.conf:/seata/config/file.conf restart: always ``` 上述配置中指定了挂载本地的 `registry.conf` 和 `file.conf` 到容器内部路径 `/seata/config/` 下。这些文件可以通过下载官方模板进行修改[^1]。 完成配置后,执行以下命令启动 Seata Server: ```bash docker-compose up -d ``` #### 修改 Seata Server 配置文件 Seata 启动成功后,可能需要调整其默认配置以适配实际需求。主要涉及两个核心配置文件:`application.yml` 和 `registry.conf`。 以下是 `application.yml` 的部分内容示例: ```yaml server: port: 8091 spring: application: name: seata-server store: mode: file client: tm: commit-retry-timeout: 5000 rollback-retry-timeout: 5000 ``` 对于 `registry.conf` 文件,则主要用于指定注册中心的相关参数: ```properties registry { type = "nacos" nacos { serverAddr = "localhost:8848" group = "SEATA_GROUP" namespace = "" } } config { type = "nacos" nacos { serverAddr = "localhost:8848" group = "SEATA_GROUP" namespace = "" } } ``` 更新完成后重新启动 Seata Server 即可生效。 #### Spring Cloud 整合 Seata 为了使微服务项目能够支持分布式事务管理功能,在引入相关依赖前,请先添加 Maven 或 Gradle 中对应的依赖项。例如针对 SpringCloud Alibaba 2021.0.4.0 及以上版本,可以加入如下内容至项目的构建工具配置文件中[^2]: Maven 示例: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2021.0.4.0</version> </dependency> ``` Gradle 示例: ```gradle implementation 'com.alibaba.cloud:spring-cloud-alibaba-seata:2021.0.4.0' ``` 随后按照业务逻辑编写代码实现全局事务控制部分即可。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值