0
点赞
收藏
分享

微信扫一扫

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题

柔性事务之Seata

关于分布式事务的几种解决方案以及刚性事务和柔性事务的概念, 在上一篇文章中已经详细介绍过, 这里不再赘述, 大家自行查看;

阿里巴巴有一个付费分布式事务框架, GTS; Seata基于阿里的GTS, 免费的开源分布式事务框架, 支持多种分布式事务模式;

XA模式

遵循 XA 规范, 采用 2PC 的方式实现分布式事务管理; 是强一致性的事务;

TCC模式

基于 TCC, 采用 TCC 的方式实现分布式事务管理; 是柔性事务;

AT模式

基于 XA/2PC 协议的变种实现, 它支持事务的回滚,但是回滚本身是由SEATA实现,并非数据库的回滚; 是强一致性的事务;

SAGA模式

SAGA模式的实现需要开发者明确定义每个子事务的补偿逻辑 (当事务的一部分操作失败时,通过执行补偿操作来撤销已经完成的部分操作),并确保所有可能的异常情况都得到处理。

适用于长业务和可以容忍临时不一致性的场景, 国内用得很少;

事务管理模型

Seata 管理的事务管理模型中有三个重要角色:

TC, 事务协调者

维护全局和分支事务的状态, 根据这些状态进行决策, 协调全局事务提交或回滚;

TM, 事务管理器

开始全局事务, 向 TC 发起提交或回滚全局事务请求;

RM, 资源管理器

管理分支事务资源(如数据库连接), 执行分支事务, 与TC交互以注册分支事务并汇报分支事务状态;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_分布式事务

开启分布式事务, 一定有一个入口方法, 在这个方法内执行本服务内部的事务逻辑, 并调用其它微服务, 形成多个分支事务;

这个方法涉及的所有分支事务, 就构成一个全局事务;

以XA模式为例, 三个对象的工作过程如下:

一阶段, 准备阶段

TM 代理了入口方法, 入口方法被调用时, TM进行拦截(代理), 向 TC 发起请求, 注册一个全局事务, 开启全局事务;

TC 会生成一个唯一的 XID 作为全局事务的编号, 返回给TM, 分支事务通过 XID 关联在一起;

RM 去代理分支业务, 通过参数传递的形式, 能够让 RM 持有其所属的全局事务的 XID;

在分支业务执行时, RM进行拦截, 向 TC 注册当前分支事务(分支事务之间通过持有相同的 XID 关联在一起), 并获取到当前分支事务的Branch ID;

然后执行分支事务, 然后向 TC 报告分支事务状态;

二阶段, 提交或回滚

入口方法整个执行完毕时, TM 向 TC 发起提交或回滚请求;

TC 收到请求, 检查分支事务的状态, 真正决定提交还是回滚, 然后驱动每个 RM 进行提交或回滚;

TM 只是发起一个二阶段的结束请求, 真正决定提交还是回滚的, 是 TC;

// 伪代码, TM 向 TC 发起提交或回滚请求
try {
    ......业务逻辑
    // 业务逻辑执行成功
    tx.commit();
} catch (Exception e) {
    tx.rollback();
}

TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

XA模式

根据 XA/2PC 实现的分布式事务解决方案;

注意

  • 异常需要层层往上抛,如果你手动 try - catch 进行异常处理的话,无法触发回滚。
  • 出现事务失效的情况下,优先检查 RootContext.getXID() ,检查xid是否传递且一致。
  • 主服务加上@GlobalTransactional注解即可,被调用服务不用加@GlobalTransactional@Transactional.
  • @GlobalTransactional(rollbackFor = Exception.class)最好加上rollbackFor = Exception.class,表示遇到Exception都回滚,不然遇到有些异常(如自定义异常)不会回滚。

使用

只需要配置使用XA模式, 并且在事务入口使用@GlobalTransactional注解即可;

seata:
  data-source-proxy-mode: XA

AT模式

也是基于 分阶段提交 的思想实现的分布式事务解决方案, 但是 Seata 实现了自己的, 数据库之上的的 UndoLog, 所以不需要数据库支持, 也能实现回滚;

在 Seata AT 模式下操作数据库时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

原理

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_Seata_02

首先, 每个业务的库中, 都要设计一张 seata 用的 UndoLog 表; Seata 生成的 UndoLog 将被保存在这张表中;

第一阶段

RM在执行分支事务时, 会生成 Undo Log, 记录数据修改前后的值, 执行成功会连同 Undo Log 一并提交到数据库, 然后报告事务状态;

执行失败则依据已经生成的 Undo Log 回滚, 并报告事务状态;

各分支事务需向 TC 注册分支 ( Branch Id) ,并为要修改的记录加锁;

第二阶段

TM 发起提交或回滚全局事务的请求;

TC 检查分支状态, 驱动 RM 提交或回滚, 之后, 对应的 Undo Log 就可删除了;

和 XA 模式比较

优势: 不需要底层数据库支持 XA事务;

本地事务分支可以在全局事务的第一阶段提交,并马上释放本地事务锁定的数据库资源。相比于传统的 XA 事务在第二阶段释放数据库资源,Seata 减小了锁的时间跨度;

劣势: 不是强一致, 不过能保证最终一致性; 需要专门设置一张 UndoLog表, 占用额外空间;

AT 模式和 XA 模式, 对业务都没有侵入;

读写隔离

详解 Seata AT 模式事务隔离级别与全局锁设计 | Apache Seata

那么 Seata 在一阶段分支事务提交后就释放底层数据库锁的操作, 站在全局事务的角度来看, 是在事务内部就把锁释放了, 这显然会在全局事务的级别上破坏一致性, 产生脏读, 丢失更新的问题;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_SpringCloud_03

seata 设置了一个全局锁来解决这个问题, 全局锁由 TC 负责记录, 具体会记录 xid, table, key, 分别为全局事务id, 表, 主键; 这样就可以让一个全局事务锁定一行数据;

默认情况下,Seata 的全局事务是工作在读未提交隔离级别的; 使用全局锁, 可以达到读已提交级别;

RM 会获取 DB 级别的行锁, 和 seata 的全局锁, 分支事务执行完毕后, 释放 DB 锁, 但全局锁保留, 二阶段提交或回滚后才释放; 以此避免脏读脏写的问题;

但是, 这只能解决全局事务之间的读写问题, 如果一个全局事务内修改某条记录, 而一个非事务的地方同时也去修改这条记录(不用获取seata全局锁), 还是会有丢失更新的问题;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_Seata_04

为此, seata 设置了前后快照, 其 UndoLog 不仅记录了SQL 执行之前的值, 也记录了执行之后的值

回滚之前, 查看当前值和后快照是否一致, 一致才可以回滚; 不一致说明有非事务的地方后来又进行了修改, 不能回滚, 抛异常;

这是一种乐观锁的思想;

使用

不需要额外配置, 默认就是 AT 模式, 在事务入口使用 @GlobalTransactional注解即可, 并不需要在分支事务添加 @Transactional 注解

seata:
  data-source-proxy-mode: AT

TCC模式

原理

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_Seata_05

区分预留操作和业务操作:

以转出余额为例, 预留操作是将冻结资金设置为要转出的金额, 将当前余额扣减要转出的金额;

业务操作是将冻结资金清零;

Cancel 操作是将冻结资金加回到当前余额;

第一阶段

TM 向 TC 发起请求, 开启全局事务; 然后调用分支事务;

RM 向 TC 注册分支事务, 进行资源预留( 例如修改冻结金额, 扣减当前余额 ):

  1. 执行 Try 方法, 如果执行成功, 直接向数据库提交预留操作;
  2. 如果失败, 立即调用 Cancel 进行回滚 ( 例如将冻结金额加回当前余额 );
  3. 无论成功还是失败, 都向 TC 汇报事务状态;

第二阶段

TM 向 TC 发起提交或回滚全局事务的请求, TC 检查分支事务状态, 决定提交还是回滚, 并驱动RM执行 Confirm 或 Cancel;

所有分支事务都 Try 成功, 进行 Confirm, 如果这个 Confirm 操作失败了怎么办?

如果是 TC 进行 rpc 时因为网络原因导致 Confirm 指令发送到 RM 失败的情况, 可以用重试机制解决这个问题;

接下来考虑 Confirm 执行过程中会不会失败;

这个 Confirm 操作一旦开始执行, 几乎不会失败, 除非机器挂了, 数据库挂了之类的;

因为在 Try 操作中已经检查了资源是否足够并进行了预留;

比如 A账户 要扣10块钱, 在 Try 操作中, 已经进行了检查, 确定余额足够, 并且暂时转移到冻结资金中进行了保留, 所以不会出现在 Confirm 阶段发现余额不足的情况;

TCC模式和AT模式, 都是在一阶段直接向数据库发起提交, 在二阶段进行检查, 如果有失败分支事务再由 Seata 进行回滚(不是数据库的回滚, 而是由Seata基于TCC或UndoLog实现的回滚); 而XA模式, 一阶段不会向数据库发起提交, 会等到二阶段一起提交或回滚(这个回滚是数据库实现的);

幂等问题

在第二阶段,TC 向 RM 发出 Confirm 或者 Cancel 指令后, 超过一定时间没有收到分支事务的响应,则认为可能是因为网络等原因使得 RM 没有收到信息, 需要进行重试;

但如果 RM 收到了指令并且成功执行了 Confirm 或 Cancel, 只是返回的响应没有到达 TC , 就会触发重试机制, 导致 RM 收到多次 Confirm 或 Cancel 指令;

这就需要 Confirm 和 Cancel 具有幂等性;

空回滚问题

在第一阶段, 不同分支事务是同步执行的, 必须等待上一个分支事务的 Try 操作成功后才能开启下一个分支事务; 这样能保证资源预留的操作是有序的, 避免在有资源预留失败的情况下继续执行后续分支事务;

而执行分支事务, 需要进行阻塞式的远程调用, 如果一个分支事务迟迟没有完成调用, 也不能无休止地等下去, 这就需要引入超时机制, 当全局事务超过一定时间还没有进入第二阶段, 就要回滚全局事务;

假设全局事务中按顺序存在 A B C 三个分支事务, 在执行 A 事务的 Try 方法时超时, 那么会触发全局事务的回滚, 此时 B 和 C 还没有执行 Try 操作, 不应该进行 Cancel , 这就是空回滚问题;

业务悬挂问题

还是 A B C 三个分支事务, A 因为网络原因, 迟迟没有收到 try 命令, 导致全局事务超时, 进行回滚 (A也会回滚, 执行Cancel, 因为Cancel 有重试机制, 可以认为 A 一定能 Cancel ); 而后 try 命令最终到达了, A 执行了 Try 操作, 但是这是全局事务已经结束, A 无法 Confirm 也无法 Cancel; 处于一个不一致状态;

解决

Seata 1.5.1 版本以后, 已经解决了TCC模式下的幂等, 空回滚, 业务悬挂问题; 不仅如此, 还顺便解决了 Confirm 和 Cancel 的幂等问题;

原理篇:Seata TCC模式是如何解决幂等性、资源悬挂、空回滚问题的-阿里云开发者社区 (aliyun.com)

在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log; 通过 @TwoPhaseBusinessAction 注解中的useTCCFence = true 来开启这个机制; 关于TwoPhaseBusinessAction注解, 后面介绍 TCC 模式使用的时候会说到;

表中包括了全局事务Id, 分支事务 Id, 事务当前的状态;

执行 try操作会先插入记录并设置 status = tried, 成功提交会设置 status = commited, 成功回滚会设置 status = rollbacked, 发生空回滚会设置 status = suspended;

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
    `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
    `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
    
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

在在@TwoPhaseBusinessAction注解中设置useTCCFence=true, 开启 TCC Fence 机制;

在一阶段进行资源预留操作之前, 检查 useTCCFence 属性, 如果设置为 true, 会调用 TCCFenceHandler.prepareFence方法;

该方法会先向 tcc_fence_log 插入一条记录, 包含当前的全局事务ID, 分支事务ID, 并且将 status 字段设置为 tried 表示该分支事务已经进入 try 阶段; 然后再进行资源预留操作;

// try 之前先向数据库添加一条有状态的记录,状态为`TCCFenceConstant.STATUS_TRIED`;
// 如果是空回滚过的事务, 则数据库中已经有了相同主键的记录, 状态为 suspended, 这里会插入失败, 也就不会预留资源
// 这就防止了业务悬挂问题;
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
if (result) {
    // 调用资源预留逻辑
    return targetCallback.execute();
} else {
    throw new TCCFenceException();
}

二阶段 RM 执行 Confirm 或者 Cancel 时, 会检查 useTCCFence 属性, 如果设置为 true, 则会调用 TCCFenceHandler 来执行当前分支事务的提交或回滚操作;

无论回滚还是提交, TCCFenceHandler 会根据当前的全局事务ID 和 分支事务ID 查询 tcc_fence_log;

TCCFenceHandler.commitFence(), 只看关键代码

public static boolean commitFence(...) {
 	// 查询一阶段提交的记录
	TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
	// 如果没查到一阶段的记录,说明没有进行 Try 操作, 不能提交
	if (tccFenceDO == null) {
		throw new TCCFenceException();
	}
	// 如果记录的状态为`committed`,说明已经提交过了,直接返回true;这就实现了 Confirm 操作幂等性;
	if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
		return true;
	}
    
    // 如果是`ROLLBACKED 或者 SUSPENDED`,说明是已经回滚的事务, 直接return, 不提交; 
	if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
		return false;
	}
	// 这里就可以执行提交逻辑,并修改状态为`COMMITTED`;
	return updateStatusAndInvokeTargetMethod(xid, branchId, STATUS_COMMITTED);
}

TCCFenceHandler.rollbackFence(), 只看关键代码

public static boolean rollbackFence(...) {
	// 查询一阶段插入的记录
	TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
	// 如果没查询到记录,说明一阶段还没执行, 这里又让回滚, 那就是空回滚的情况;
	if (tccFenceDO == null) {
        // 空回滚时要插入一条记录,将事务状态设置为STATUS_SUSPENDED
        // 后续如果又进行 Try, 就会因为主键冲突插入失败, 避免执行 Try, 这样就避免了业务悬挂;
		boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);         
    }

	// 如果是`rollback`状态,说明已经回滚过了;直接返回
    // 如果是`SUSPENDED`,空回滚的情况, 也不允许执行再次回滚逻辑; 直接返回
    // 实现了 Cancel 的幂等性;
	if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
		return true;
        
	// 如果是`TCCFenceConstant.STATUS_COMMITTED`状态,说明已经提交了,那么也不能回滚;
	if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus())
        return false;

    // 执行回滚逻辑,并修改状态为`TCCFenceConstant.STATUS_ROLLBACKED`;
 	return updateStatusAndInvokeTargetMethod(conn, rollbackMethod,ROLLBACKED);
}

使用 TCC 模式

不需要修改 yaml 配置文件;

创建TCC接口, 在接口上添加@LocalTCC注解, 定义三个抽象方法, 在 Try 接口上使用TwoPhaseBusinessAction注解进行标注, 指定 Confirm 和 Cancel 接口对应的方法名;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_分布式事务_06

然后定义实现类, 并实现这三个方法;

然后在会调用 try 方法的业务方法上添加 @GlobalTransactional注解;

@GlobalTransactional
@PostMapping("/transfer")
public String transfer(@RequestParam String fromAccountId, @RequestParam String toAccountId, @RequestParam double amount) {
	// 调用 TCC 的 Try 方法
	boolean result = tccService.prepare(null);
	if (result) {
	    // 如果成功,Seata 会自动调用 Confirm 方法
	    return "Transfer successful";
	} else {
	    // 如果失败,Seata 会自动调用 Cancel 方法
	    return "Transfer failed";
	}
}

Seata 部署

部署单机TC

以Windows为例, Linux略有出入, Seata 极简入门 | Apache Seata

单机 Seata TC Server,常用于学习或测试使用,不建议在生产环境中部署单机。

因为 TC 需要进行全局事务和分支事务的记录,所以需要对应的存储支持。目前,TC 有两种存储模式( store.mode ):

  • file 模式:适合单机模式,全局事务会话信息在 内存 中读写,并持久化本地文件 root.data,性能较高。
  • db 模式:适合集群模式,全局事务会话信息通过 db 共享,相对性能差点。

下载安装包

地址: https://github.com/apache/incubator-seata/releases/download/v1.4.2/seata-server-1.4.2.zip

具体下什么版本, 可以到你项目的依赖版本管理中去搜索 seata, 例如这里是 1.4.2

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-alibaba-dependencies</artifactId>
	<version>2.2.2.RELEASE</version>
	<type>pom</type>
	<scope>import</scope>
</dependency>

<!-- 该版本需要的 TC 服务器版本是 1.3.0以上, 推荐使用1.4.2 -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	<version>2.2.2.RELEASE</version>
</dependency>

执行脚本

在 安装目录 / bin 目录下执行seata-server.bat即可开启TC服务, 默认运行在8091端口, 使用 file模式;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_SpringCloud_07

部署TC集群

以Windows为例, Linux略有出入, Seata 极简入门 | Apache Seata

在集群时,多个 Seata TC Server 通过 db 数据库,实现全局事务会话信息的共享。

可以使用注册中心, 将多个 TC Server 注册到注册中心, 客户端直接从注册中心获取 TC Server 的可用实例;

Seata 对主流的注册中心都支持, 这里选择Nacos;

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_分布式事务_08

和单机一样, 先下载并解压;

建立数据库

下载源码 https://codeload.github.com/apache/incubator-seata/zip/refs/tags/v1.4.2

在MySQL中, 建一个库, 取名为seata, 在该库下执行源码根目录/script/server/db/mysql.sql;

修改配置文件

修改 seata 根目录/conf/file.conf 配置文件, 修改存储方式

后续使用 Nacos 配置中心后, 这里就不用管了, 会在在配置中心中添加这些配置;

## transaction log store, only used in seata-server
store {
  # 修改为 db
  mode = "db"

  # 配置DB
  db {
  	# mysql8 要用com.mysql.cj.jdbc.Driver
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
  }
}

修改conf/registry.conf, 配置Nacos

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  # 对应注册中心
  nacos {
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    # 用默认的 public 空间
    namespace = ""
    # 不能用汉字
    cluster = "Suzhou"
    username = ""
    password = ""
  }
}

# 对应配置中心
config {
  # 默认是file, 对应file.conf文件, 如果改为nacos, 需要将file.conf迁移到nacos配置中心
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "DEFAULT_GROUP"
    username = ""
    password = ""
    # 指定 dataId, 很重要
    dataId = "seataServer.properties"
  }
}

Nacos中添加配置

创建seataServer.properties, 给 Seata TC server 用;

注意 mysql8.0 使用com.mysql.cj.jdbc.Driver

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

启动

正常情况下启动多个服务器, 作为一个集群, 应该是在不同的主机上, 使用相同的端口 8091;

现在为了学习, 在一个主机上启动多个端口不同的服务器, 将 seata 安装目录复制一份, 修改registry.conf, 修改集群名称, 模拟不同地域的主机;

可以使用相同的 Nacos 配置集

registry {
  type = "nacos"

  nacos {
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "HangZhou"
    username = ""
    password = ""
  }
}

config {
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "DEFAULT_GROUP"
    username = ""
    password = ""
    dataId = "seataServer.properties"
  }
}

本地文件

[ 分布式事务 ] (二) Seata 单机与集群部署及三种工作模式原理详解, 教你如何解决 TCC 空回滚, 业务悬挂问题_Seata_09

启动时, 要手动指定端口号才能实现在非8091端口启动

.\seata-server.bat -p 8091 -n 1
.\seata-server.bat -p 8092 -n 2

  • -p:Seata TC Server 监听的端口。
  • -n:Server node。在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突。

客户端配置

导包

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	<exclusions>
        <!-- 版本太低, 排除掉 -->
	    <exclusion>
	        <groupId>io.seata</groupId>
	        <artifactId>seata-spring-boot-starter</artifactId>
	    </exclusion>
	</exclusions>
</dependency>
<!-- 单独引入新版本 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

配置 undo log

提供给 AT 模式使用; 有了这张表, 不需要数据库本身支持回滚操作, 也能实现回滚;

源码script/client/at/db/mysql.sql提供了 sql 文件;

每个服务的数据库中, 都需要有这张表;

微服务增加配置

每个涉及的服务都要进行配置, 通过这些配置就能从 Nacos 发现指定的 seata 服务;

采用本地配置如下, 这样就能找到 nacos服务器 -- 命名空间 -- group -- cluster

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置, 这里是要去注册中心找 seata 服务
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server
  # 定义事务组, 非常重要, 参与同一个分布式事务的服务, 应该在同一个事务组下
  tx-service-group: seata-demo 
  service:
    vgroup-mapping: # 配置我们自定义的事务组, 与 seata 集群的映射关系
      seata-demo: SuZhou # 在上面定义了一个事务组 seata-demo, 这里配置一个映射, 映射到SuZhou集群

而如果客户端使用采用了 Nacos 配置中心, 在bootstrap.yml进行如下配置:

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: 
      password: 
      group: DEFAULT_GROUP
      data-id: seataClient.properties

nacos 中建立 seataClient.properties 配置集

# 事务组映射关系, 修改这里就能实现动态切换集群, 不用重启微服务;
service.vgroupMapping.seata-demo=SuZhou

service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
# RM配置
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
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000

# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100

配置代理数据源

Seata 实现分布式事务时, 需要代理 Mybatis 数据源, 拦截 SQL 操作, 从而完成分支事务的开启, 提交与回滚操作; 在AT模式下, 在 DML 语句执行时, 还要将更新前后的记录保存到 undo_log 中; 所以参与分布式事务的每个服务, 都要配置代理数据源

@Configuration
public class SeataDataSourceConfig {
	@Bean
	@ConfigurationProperties(prefix = "spring.datasource")
	public DataSource druidDataSource() {
		return new DruidDataSource();
	}
	//创建代理数据源
	@Bean
	public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
		return new DataSourceProxy(druidDataSource);
	}
	//替换MybatisSqlSessionFactoryBean的DataSource
	@Bean
	public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSourceProxy
	dataSourceProxy) throws Exception {
		// 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则MyBatisPlus 不会生效
		MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new
														MybatisSqlSessionFactoryBean();
		
        mybatisSqlSessionFactoryBean.setDataSource(dataSourceProxy);
		return mybatisSqlSessionFactoryBean;
	}
}

举报

相关推荐

0 条评论