XA 协议是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。目前主流的数据库,比如 oracle、DB2 、mysql(5.0以后)都是支持 XA 协议的,你可以把 XA 理解为一个强一致性的中心化原子提交协议。
2PC:它就是把一个事务分成了两步来提交。第一步做准备动作,第二步做提交 / 回滚动作,这两步之间的协调是交由一个中心化的 Coordinator 来管理,保证多步操作的原子性。
第一步(Prepare):Coordinator 向各个分布式事务的参与者下达了 Prepare 指令,各个事务分别将 SQL 语句在数据库执行但不提交,并且将就绪状态上报给 Coordinator。
第二步(Commit/Rollback):如果所有节点都已就绪,那么 Coordinator 就下达 Commit 指令,各个参与者提交本地事务;如果有任何一个节点不能就绪,Coordinator 则下达 Rollback 指令进行本地回滚。
这种分布式事务有一个天然缺陷,导致 XA 特别不适合用在互联网的高并发场景里面。因为每个本地事务在 Prepare 阶段,都要一直占用一个数据库连接资源,这个资源直到二阶段 Commit 或者 Rollback 之后才会被释放。
使用场景:对事物有强一致要求,对事务执行效率要求不高,并且不希望有太多的代码侵入
TCC模式可以解决2pc中资源锁定和阻塞的问题,减少资源占用问题
它本质是一种补偿的思路,事务运行包括三个方法
执行分两个阶段:
准备阶段(try):资源的检测和预留
执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法,如果上一步中所有事物参与者都成功,则这里执行confirm.反之,执行cancel
使用场景:
基本的设计思想就是将远程分布式事务拆分成一系列的本地事务.
基本原理
一般分为事务的发起者A和其他参与者B:
几个注意事项:
2019年一月份,Seata开源了AT模式,AT模式是一种无侵入的分布式事务解决方案.可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题.
在AT模式下,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Stata框架会自动生成事务的二阶段提交和回滚操作.
基本原理:
两个阶段:
但AT模式底层做的事情完全不同,而且二阶段不需要我们编写.全部由Seata自己实现了,也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务.
那么,AT模式如何实现无代码侵入,如何帮我们自动实现二阶段代码?
一阶段
在一阶段,Seata会拦截业务SQL,首先解析SQL预约,找到业务sql要更新的业务数据,在业务数据被更新前,将其保存成"before image",然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成after image,最后获取全局行锁,提交事务,以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性.
这里的before image和agter image类似于数据库的undo和redo日志,但其实使用数据库模拟的.
二阶段
提交:
因为业务sql在一阶段已经提交至数据库了,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可.
回滚:
Seata就需要回滚一阶段已经执行的业务sql,还原业务数据,回滚方式便是用before image还原业务数据;但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏鞋,出现脏鞋就需要转人工处理.
不过因为有全局锁机制,可以降低出现脏鞋的概率
AT模式的一阶段,二阶段提交和回滚都由Seata框架自动完成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
Seata中的几个基本概念:
TC(Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)。
TM(Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM(Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
我们看下面的一个架构图
一阶段:
before_image
after_image
undo_log
并写入数据库二阶段:
before_image
和after_image
信息,释放全局锁before_image
,清除before_image
和after_image
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
适用场景:
优势:
缺点:
在之前讲解Seata原理的时候,我们就聊过,其中包含重要的3个角色:
其中,TC是一个独立的服务,负责协调各个分支事务,而TM和RM通过jar包的方式,集成在各个事务参与者中。
因此,首先我们需要搭建一个独立的TC服务。
首先去官网下载TC的服务端安装包,GitHub的地址:https://github.com/seata/seata/releases
然后解压即可,其目录结构如下:
包括:
Seata的核心配置主要是两部分:
${seata_home}/conf/
目录中,一般是registry.conf
文件registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 指定注册中心类型,这里使用nacos类型
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.10.101:8848"
group = "SEATA_GROUP"
namespace = "e3aa94a8-47cf-4737-ab96-c664eeb61f8b"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
# 配置文件方式,可以支持 file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
file.conf
文件:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://192.168.10.101:3306/dev_seata?rewriteBatchedStatements=true"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
但是如果使用db作为存储介质,还需要在数据库中创建3张表:
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 = utf8;
-- 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,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
进入${seata_home}/bin/
目录中:
如果是linux环境(要有JRE),执行seata-server.sh
如果是windows环境,执行seata-server.bat
undo_log
CREATE TABLE `undo_log` (
`branch_id` bigint(20) 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`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='AT transaction mode undo table'
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>2021.1version>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
首先在application.yml中添加一行配置:
spring:
cloud:
alibaba:
seata:
tx-service-group: test_tx_group # 定义事务组的名称
这里是定义事务组的名称,接下来会用到。
然后是在resources
目录下放两个配置文件:file.conf
和registry.conf
其中,registry.conf
与TC服务端的一样,此处不再讲解。
我们来看下`file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroup_mapping.test_tx_group = "seata_tc_server"
#only support when registry.type=file, please don't set multiple addresses
seata_tc_server.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
# 二阶段回滚镜像校验 默认true开启,false关闭。
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
或者yaml配置:
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: 192.168.10.101:8848
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 192.168.10.101:8848
group: SEATA_GROUP
username: nacos
password: nacos
service:
vgroup-mapping:
test_tx_group: default
disable-global-transaction: false
client:
rm:
report-success-enable: false
配置解读:
transport
:与TC交互的一些配置
heartbeat
:client和server通信心跳检测开关enableClientBatchSendRequest
:客户端事务消息请求是否批量合并发送service
:TC的地址配置,用于获取TC的地址
vgroup_mapping.test_tx_group = "seata_tc_server"
:
test_tx_group
:是事务组名称,要与application.yml中配置一致,seata_tc_server
:是TC服务端在注册中心的id,将来通过注册中心获取TC地址enableDegrade
:服务降级开关,默认关闭。如果开启,当业务重试多次失败后会放弃全局事务disableGlobalTransaction
:全局事务开关,默认false。false为开启,true为关闭default.grouplist
:这个当注册中心为file的时候,才用到client
:客户端配置
rm
:资源管理器配
asynCommitBufferLimit
:二阶段提交默认是异步执行,这里指定异步队列的大小lock
:全局锁配置
retryInterval
:校验或占用全局锁重试间隔,默认10,单位毫秒retryTimes
:校验或占用全局锁重试次数,默认30次retryPolicyBranchRollbackOnConflict
:分支事务与其它全局回滚事务冲突时锁策略,默认true,优先释放本地锁让回滚成功reportRetryCount
:一阶段结果上报TC失败后重试次数,默认5次tm
:事务管理器配置
commitRetryCount
:一阶段全局提交结果上报TC重试次数,默认1rollbackRetryCount
:一阶段全局回滚结果上报TC重试次数,默认1undo
:undo_log的配置
dataValidation
:是否开启二阶段回滚镜像校验,默认truelogSerialization
:undo序列化方式,默认JacksonlogTable
:自定义undo表名,默认是undo_log
log
:日志配置
exceptionRate
:出现回滚异常时的日志记录频率,默认100,百分之一概率。回滚失败基本是脏数据,无需输出堆栈占用硬盘空间Seata的二阶段执行是通过拦截sql语句,分析语义来指定回滚策略,因此需要对DataSource做代理。我们在项目的cn.itcast.order.config
包中,添加一个配置类:
package cn.itcast.order.config;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceProxyConfig {
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
// 订单服务中引入了mybatis-plus,所以要使用特殊的SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
// 代理数据源
sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource));
// 生成SqlSessionFactory
return sqlSessionFactoryBean.getObject();
}
}
注意,这里因为订单服务使用了mybatis-plus这个框架(这是一个mybatis集成框架,自动生成单表Sql),因此我们需要用mybatis-plus的MybatisSqlSessionFactoryBean
代替SqlSessionFactoryBean
如果用的是原生的mybatis,请使用SqlSessionFactoryBean
。
或者
seata:
enabled: true
enable-auto-data-source-proxy: true
给事务发起者order_service
的OrderServiceImpl
中的createOrder()
方法添加@GlobalTransactional
注解,开启全局事务:
重新启动即可。
Seata AT模式下,二阶段-回滚。会做数据校验。
数据校验:拿 UNDO LOG 中的数据与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
这个所谓的文档,在官网找了半天,才找到。大概意思是,可以将数据校验功能关闭(默认是打开的)。关闭了就不报错了。
通过设置client端的一个参数,将校验功能关闭:
client.undo.dataValidation 二阶段回滚镜像校验 默认true开启,false关闭。
(PS:不过把这个关闭了,有点草率了吧。直接回滚?中间乱入的数据不处理了,会出问题的。)
可以看出Seata AT模式一阶段执行本地事务的时候,才会锁表/行。一阶段后本地事务就已经提交了,也就释放了锁,别的操作可以更改数据。如果二阶段发生回滚,数据会有异常,需要额外处理(人工介入等等)。反正官网没给出解决方案,所谓的解决方案只是关闭校验。不可行。
无法回滚之后会seata会带有行锁
回滚日志也不会清除
错误信息:Cause: io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
解决方案:找出程序中对该表有修改操作的方法,添加@GlobalLock添加全局锁
GlobalLock 注解说明
从执行过程和提交过程可以看出,既然开启全局事务 @GlobalTransactional注解可以在事务提交前,查询全局锁是否存在,那为什么 Seata 还要设计多处一个 @GlobalLock注解呢?
因为并不是所有的数据库操作都需要开启全局事务,而开启全局事务是一个比较重的操作,需要向 TC 发起开启全局事务等 RPC 过程,而@GlobalLock注解只会在执行过程中查询全局锁是否存在,不会去开启全局事务,因此在不需要全局事务,而又需要检查全局锁避免脏读脏写时,使用@GlobalLock注解是一个更加轻量的操作。