Seata分布式事务
在单体项目中,一般涉及到的数据源是唯一的,我们可以使用数据库提供的本地事务保证数据的一致性。但是在微服务或者多数据源的项目中,涉及到跨库操作,单个数据库的本地事务无法保证不同数据库的数据一致性。
Seata 提供了 AT
、TCC
、SAGA
和 XA
事务模式,打造一站式的分布式解决方案,设计思路是将一个分布式事务理解成全局事务,里面包含若干分支事务,每个分支事务是一个满足ACID的本地事务,因此我们可以操作分布式事务像操作本地事务一样。
Seata定义了3个模块来处理全局事务和分支事务的关系和处理过程:
- **TC (Transaction Coordinator) - 事务协调者:**维护全局和分支事务的状态,驱动全局事务提交或回滚。
- **TM (Transaction Manager) - 事务管理器:**定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- **RM (Resource Manager) - 资源管理器:**管理分支事务处理的资源,与TC交谈已注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
TM 和 RM 作为 Seata 客户端集成在我们的项目业务中,TC 作为 Seata 的服务端独立部署。
处理过程:
- TM 向 TC 申请开启一个全局事务,TC 创建全局事务后返回全局唯一的 XID,XID 会在全局事务的上下文中传播;
- RM 向 TC 注册分支事务,该分支事务归属于拥有相同 XID 的全局事务;
- TM 要求 TC 提交或回滚 XID 的相应全局事务;
- TC 在 XID 的相应全局事务下驱动所有分支事务以完成分支提交或回滚。
AT模式(Automatic Transaction)
一、整体机制:
AT 模式本质上采用了两阶段提交协议,并在原有基础上进行了改进:
一阶段:
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志(undo log),利用本地事务的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。这样可以保证任何提交的业务数据的更新一定有相应的回滚日志存在,最后对分支事务状态向 TC 进行上报。基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,马上释放本地事务锁定的资源。
二阶段:
- 全局提交:此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志)
- 全局回滚:RM 收到 TC 发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新SQL并执行,以完成分支的回滚。
二、隔离:
AT 模式引入全局锁机制来实现隔离,全局锁是由 TC 维护。
-
写隔离:
- 第一阶段本地事务提交前,需要确保先拿到全局锁 。
- 拿不到全局锁,不能提交本地事务。
- 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
-
读隔离:
在数据库本地事务隔离级别为读已提交(READ COMMITTED)或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交(READ UNCOMMITTED)。如果应用在特定场景下,必需要求全局的读已提交,目前 Seata 的方式是通过SELECT FOR UPDATE语句的代理。
SELECT FOR UPDATE语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。这个过程中,查询是被阻塞 住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。
TCC模式(Try-Confirm-Cancel)
AT 模式是基于支持本地 ACID 事务的关系型数据库进行了:一阶段的prepare、二阶段的commit或者二阶段的rollback。TCC 模式,则不依赖于底层数据资源的事务支持,而是通过对业务逻辑的分解,把自定义的分支事务纳入到全局事务的管理中,以此来实现分布式事务。
TCC分布式事务模型包括三部分:
- 主业务服务(Main Server):主业务服务为整个业务活动的发起方、服务的编排者,负责发起并完成整个业务活动。
- 从业务服务(Service):从业务服务是整个业务活动的参与方,负责提供 TCC 业务操作,实现Try、Confirm、Cancel三个接口,供主业务服务调用。
- 事务管理器(Transaction Manager):事务管理器管理控制整个业务活动,包括记录维护TCC全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的Confirm操作,在业务活动取消时调用所有从业务服务的Cancel操作。
Saga模式
Saga算法是一种异步的分布式解决方案。其理论基础在于,其假设所有事件按照顺序推进,总能达到系统的最终一致性,因此 Saga需要服务分别定义提交接口以及补偿接口,当某个事务分支失败时,调用其它的分支的补偿接口来进行回滚。
在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
saga一般有两种实现,一种是基于状态机定义,比如apache camel saga、eventuate,一种是基于注解+拦截器实现,比如serviceComb saga,后者是不需要配置状态图的。
XA模式(eXtended Architecture)
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
一、整体机制:
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
XA 的各个分支事务是在数据库层面上驱动的,XA 方案的 RM 是放在数据库层的,它依赖了数据库的 XA 驱动程序。
搭建与使用(集成Nacos)
一、安装seata服务端
-
下载seata服务端压缩包,解压后修改conf/registry.conf配置文件:
registry { // 注册中心设置 type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "nacos" password = "nacos" } } config { // 配置中心设置 type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" } }
-
进入seata安装目录下的bin,启动seata服务端。
## Linux下: sh ./seata-server.sh ## windows下: 双击 seata-server.bat
二、配置数据库
-
seata 使用 mysql 作为 db 高可用数据库,故为创建一个mysql数据库,脚本如下:
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; -- 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(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; -- 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(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) 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';
-
为自己项目的业务库导入undo_log表:
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 = utf8 COMMENT ='AT transaction mode undo table';
三、导入配置到nacos
-
在seata目录下添加config.txt文件,内容如下:
# 设置事务组号值为default,事务组编号在项目配置seata.tx-service-group service.vgroupMapping.my_test_tx_group=default store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true # 指向我们上面创建的seata数据库 store.db.user=username store.db.password=password 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
-
复制nacos-config.sh到seata的conf目录下:
#!/usr/bin/env bash # Copyright 1999-2019 Seata.io Group. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at、 # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. while getopts ":h:p:g:t:u:w:" opt do case $opt in h) host=$OPTARG ;; p) port=$OPTARG ;; g) group=$OPTARG ;; t) tenant=$OPTARG ;; u) username=$OPTARG ;; w) password=$OPTARG ;; ?) echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] " exit 1 ;; esac done urlencode() { for ((i=0; i < ${#1}; i++)) do char="${1:$i:1}" case $char in [a-zA-Z0-9.~_-]) printf $char ;; *) printf '%%%02X' "'$char" ;; esac done } if [[ -z ${host} ]]; then host=localhost fi if [[ -z ${port} ]]; then port=8848 fi if [[ -z ${group} ]]; then group="SEATA_GROUP" fi if [[ -z ${tenant} ]]; then tenant="" fi if [[ -z ${username} ]]; then username="" fi if [[ -z ${password} ]]; then password="" fi nacosAddr=$host:$port contentType="content-type:application/json;charset=UTF-8" echo "set nacosAddr=$nacosAddr" echo "set group=$group" failCount=0 tempLog=$(mktemp -u) function addConfig() { curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$(urlencode $1)&group=$group&content=$(urlencode $2)&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null if [[ -z $(cat "${tempLog}") ]]; then echo " Please check the cluster status. " exit 1 fi if [[ $(cat "${tempLog}") =~ "true" ]]; then echo "Set $1=$2 successfully " else echo "Set $1=$2 failure " (( failCount++ )) fi } count=0 for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do (( count++ )) key=${line%%=*} value=${line#*=} addConfig "${key}" "${value}" done echo "=========================================================================" echo " Complete initialization parameters, total-count:$count , failure-count:$failCount " echo "=========================================================================" if [[ ${failCount} -eq 0 ]]; then echo " Init nacos config finished, please start seata-server. " else echo " init nacos config fail. " fi
-
执行nacos-config.sh将config.txt文件内容导入到nacos配置中心:
命令的ip地址是nacos的IP地址,执行结束可在nacos配置中查看到config.txt的内容
sh nacos-config.sh -h 127.0.0.1
四、项目配置:
-
application.yml文件添加seata配置:
# seata配置 seata: enabled: true # Seata 应用编号,默认为 ${spring.application.name} application-id: ${spring.application.name} # Seata 事务组编号,用于 TC 集群名 tx-service-group: ${spring.application.name}-group # 关闭自动代理 enable-auto-data-source-proxy: false # 服务配置项 service: # 虚拟组和分组的映射 vgroup-mapping: mgi-manager-group: default config: type: nacos nacos: serverAddr: host.docker.internal:8848 group: SEATA_GROUP namespace: registry: type: nacos nacos: application: seata-server server-addr: host.docker.internal:8848 namespace: spring: datasource: dynamic: seata: true
-
添加注解开启全局事务:
@GlobalTransactional(rollbackFor = Exception.class)
注意:
@GlobalTransactional负责开启全局事务,只与seata服务端交互,而@Transactional管理的是本地数据库的事务,两者可以同时使用,但是本地事务@Transactional的传播特性需设置为 REQUIRES_NEW
**总结:**上面的使用是 Seata 的 AT 模式的用法,Seata 帮我们在数据源上做了代理,数据源代理部分主要有三类Proxy:DataSourceProxy、ConnectionProxy和StatementProxy。XA 模式与 AT 模式用法类似,但是 XA 模式需要XAConnection,在编程模型上,只要修改数据代理源,即可实现 XA 模式与 AT 模式之间的切换。
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}