柔性事务之Seata
关于分布式事务的几种解决方案以及刚性事务和柔性事务的概念, 在上一篇文章中已经详细介绍过, 这里不再赘述, 大家自行查看;
阿里巴巴有一个付费分布式事务框架, GTS; Seata基于阿里的GTS, 免费的开源分布式事务框架, 支持多种分布式事务模式;
XA模式
遵循 XA 规范, 采用 2PC 的方式实现分布式事务管理; 是强一致性的事务;
TCC模式
基于 TCC, 采用 TCC 的方式实现分布式事务管理; 是柔性事务;
AT模式
基于 XA/2PC 协议的变种实现, 它支持事务的回滚,但是回滚本身是由SEATA实现,并非数据库的回滚; 是强一致性的事务;
SAGA模式
SAGA模式的实现需要开发者明确定义每个子事务的补偿逻辑 (当事务的一部分操作失败时,通过执行补偿操作来撤销已经完成的部分操作),并确保所有可能的异常情况都得到处理。
适用于长业务和可以容忍临时不一致性的场景, 国内用得很少;
事务管理模型
Seata 管理的事务管理模型中有三个重要角色:
TC, 事务协调者
维护全局和分支事务的状态, 根据这些状态进行决策, 协调全局事务提交或回滚;
TM, 事务管理器
开始全局事务, 向 TC 发起提交或回滚全局事务请求;
RM, 资源管理器
管理分支事务资源(如数据库连接), 执行分支事务, 与TC交互以注册分支事务并汇报分支事务状态;
开启分布式事务, 一定有一个入口方法, 在这个方法内执行本服务内部的事务逻辑, 并调用其它微服务, 形成多个分支事务;
这个方法涉及的所有分支事务, 就构成一个全局事务;
以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 用的 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 设置了一个全局锁来解决这个问题, 全局锁由 TC 负责记录, 具体会记录 xid, table, key
, 分别为全局事务id, 表, 主键; 这样就可以让一个全局事务锁定一行数据;
默认情况下,Seata 的全局事务是工作在读未提交隔离级别的; 使用全局锁, 可以达到读已提交级别;
RM 会获取 DB 级别的行锁, 和 seata 的全局锁, 分支事务执行完毕后, 释放 DB 锁, 但全局锁保留, 二阶段提交或回滚后才释放; 以此避免脏读脏写的问题;
但是, 这只能解决全局事务之间的读写问题, 如果一个全局事务内修改某条记录, 而一个非事务的地方同时也去修改这条记录(不用获取seata全局锁), 还是会有丢失更新的问题;
为此, seata 设置了前后快照, 其 UndoLog 不仅记录了SQL 执行之前的值, 也记录了执行之后的值
回滚之前, 查看当前值和后快照是否一致, 一致才可以回滚; 不一致说明有非事务的地方后来又进行了修改, 不能回滚, 抛异常;
这是一种乐观锁的思想;
使用
不需要额外配置, 默认就是 AT 模式, 在事务入口使用 @GlobalTransactional
注解即可, 并不需要在分支事务添加 @Transactional 注解
seata:
data-source-proxy-mode: AT
TCC模式
原理
区分预留操作和业务操作:
以转出余额为例, 预留操作是将冻结资金设置为要转出的金额, 将当前余额扣减要转出的金额;
业务操作是将冻结资金清零;
Cancel 操作是将冻结资金加回到当前余额;
第一阶段
TM 向 TC 发起请求, 开启全局事务; 然后调用分支事务;
RM 向 TC 注册分支事务, 进行资源预留( 例如修改冻结金额, 扣减当前余额 ):
- 执行 Try 方法, 如果执行成功, 直接向数据库提交预留操作;
- 如果失败, 立即调用 Cancel 进行回滚 ( 例如将冻结金额加回当前余额 );
- 无论成功还是失败, 都向 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 接口对应的方法名;
然后定义实现类, 并实现这三个方法;
然后在会调用 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
模式;
部署TC集群
以Windows为例, Linux略有出入, Seata 极简入门 | Apache Seata
在集群时,多个 Seata TC Server 通过 db 数据库,实现全局事务会话信息的共享。
可以使用注册中心, 将多个 TC Server 注册到注册中心, 客户端直接从注册中心获取 TC Server 的可用实例;
Seata 对主流的注册中心都支持, 这里选择Nacos;
和单机一样, 先下载并解压;
建立数据库
下载源码 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"
}
}
本地文件
启动时, 要手动指定端口号才能实现在非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;
}
}