在微服务架构中,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一 致性。
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
两阶段提交(Two Phase Commit
),就是将提交(commit
)过程划分为两个阶段(Phase
):
TM
通知各个RM
准备提交它们的事务分支。如果RM
判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给TM
肯定答复;要是发生了其他情况,那给TM
的都是否定答复。mysql
数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出==prepare
"准备提交"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成commit
"可以提交"==,然后把结果返回给事务管理器。TM
根据阶段1各个RM prepare
的结果,决定是提交还是回滚事务。如果所有的RM
都prepare
成功,那么TM
通知所有的RM
进行提交;如果有RM prepare
失败的话,则TM
通知所有RM
回滚自己的事务分支。mysql
数据库为例,如果第一阶段中所有数据库都prepare
成功,那么事务管理器向数据库服务器发出=="确认提交"请求,数据库服务器把事务的"可以提交"状态改为"提交完成"状态==,然后返回应答。如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会 把=="可以提交"的事务回撤==两阶段提交方案下全局事务的ACID
特性,是依赖于RM
的。一个全局事务内部包含了多个独立的事务分支,这一组事务分支要么都成功,要么都失败。各个事务分支的ACID
特性共同构成了全局事务的ACID
特性。也就是将单个事务分支支持的ACID
特性提升一个层次到分布式事务的范畴。
同步阻塞问题
2PC 中的参与者是阻塞的。在第一阶段收到请求后就会预先锁定资源,一直到 commit
后才会释放。
单点故障
由于协调者的重要性,一旦协调者TM
发生故障,参与者RM
会一直阻塞下去。尤其在第二阶段, 协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
数据不一致
若协调者第二阶段发送提交请求时崩溃,可能部分参与者收到commit
请求提交了事务,而另一 部分参与者未收到commit
请求而放弃事务,从而造成数据不一致的问题。
Seata
是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata
将为用户提供了 AT、TCC、SAGA 和 XA
事务模式,为用户打造一站式的分布式解决方案。AT
模式是阿里首推的模式,阿里云上有商用版本的GTS
(Global Transaction Service 全局事务服务)
官网:https://seata.io/zh-cn/index.html
源码: https://github.com/seata/seata
在 Seata
中,一个分布式事务的生命周期如下:
Seata 事务目前支持 INSERT、UPDATE、DELETE 三类 DML 语法的部分功能,这些类型都是已经经过 Seata 开源社区的验证。SQL 的支持范围还在不断扩大,建议在本文限制的范围内使用。如果您有意帮助社区支持更多类型的 SQL,请提交 PR 申请。
// use JdbcTemplate
public void batchUpdate() {
jdbcTemplate.batchUpdate(
"update storage_tbl set count = count -1 where id = 1",
"update storage_tbl set count = count -1 where id = 2"
);
}
// use Statement
public void batchUpdateTwo() {
statement.addBatch("update storage_tbl set count = count -1 where id = 1");
statement.addBatch("update storage_tbl set count = count -1 where id = 2");
statement.executeBatch();
}
下载:seata.io
Seata
分TC
、TM
和RM
三个角色,TC
(Server端)为单独服务端部署,TM
和RM
(Client端)由业务系统集成。
资源目录:
client
端sq
l脚本,参数配置config.txt
(包含server
和client
)为通用参数文件修改配置文件
配置文件的位置在/seata/config
下
配置中心设置
如果
nacos
上的配置与application.yml
有相同配置,则覆盖
示例配置
实际配置(使用nacos
)
向nacos
上传配置文件
直接官方提供的即可,\script\config-center\config.txt
,复制文件内容,在nacos
上按照上述配置创建配置文件
TC 端注册中心设置
实际配置(使用nacos
)
日志配置
存储模式配置
Server
端存储模式(store.mode
)支持三种:
root.data
,性能较高db
共享,相应性能差些mysql、oracle、postgresql
实际配置(使用db
)
在naocs
配置中心修改配置文件
由于seata
是通过jdbc
的executeBatch
来批量插入全局锁的,根据MySQL
官网的说明,连接参数中的rewriteBatchedStatements
为true
时,在执行executeBatch
,并且操作类型为insert
时,jdbc
驱动会把对应的SQL
优化成insert into () values (), ()
的形式来提升批量插入的性能。根据实际的测试,该参数设置为true
后,对应的批量插入性能为原来的10倍多,因此在数据源为MySQL
时,建议把该参数设置为true
。
创建需要的表(mysql)
在配置中的数据库下,执行\seata\script\server\db\mysql.sql
文件即可
事务分组设置
配置事务分组, 之后client
端配置需与其的事务分组一致
seata
的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。seata-server
服务端一个或多个节点组成的集群cluster
。 应用程序(客户端)使用时需要指定事务逻辑分组与Seata
服务端集群的映射关系。在naocs
配置中心修改配置文件
事务分组如何找到后端Seata集群(TC)?
GlobalTransactionScanner
构造方法的txServiceGroup
参数)。若应用程序是SpringBoot
则通过seata.tx-service-group
配置。service.vgroupMapping .[事务分组配置项]
,取得配置项的值就是TC
集群的名称。若应用程序是SpringBoot
则通过seata.service.vgroup-mapping.事务分组名=集群名称
配置Seata-Server
已经完成服务注册,且Seata-Server
向注册中心报告cluster
名与应用程序(客户端)配置的集群名称一致)TC
服务列表(即Seata-Server
集群节点列表)启动Seata Server
双击/bin/seata-server.bat
启动
在nacos
上注册成功
支持的启动参数
参数 | 全写 | 作用 | 备注 |
---|---|---|---|
-h | –host | 指定在注册中心注册的 IP | 不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定 |
-p | –port | 指定 server 启动的端口 | 默认为 8091 |
-m | –storeMode | 事务日志存储方式 | 支持file,db,redis,默认为 file 注:redis需seata-server 1.3版本及以上 |
-n | –serverNode | 用于指定seata-server节点ID | 如 1,2,3…, 默认为 1 |
-e | –seataEnv | 指定 seata-server 运行环境 | 如 dev, test 等, 服务启动时会使用 registry-dev.conf 这样的配置 |
比如:
bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db
AT
模式依赖于seata
的undo_log
回滚日志,来进行事务的回滚。所以需要为每个微服务的对应的库创建undo_log
日志表(如果都在同一个库则仅需要一张undo_log
表即可)。
-- 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 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
TM 端
maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.6.0version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
dependencies>
application.tml 配置
server:
port: 7100
spring:
application:
name: xa-order
cloud:
# 应用自身nacos注册地址
nacos:
discovery:
server-addr: 127.0.0.1:8848
password: nacos
username: nacos
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
# seata配置需保持和server端一致
seata:
application‐id: ${spring.application.name}
# 数据源代理模式 默认AT
data-source-proxy-mode: XA
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx‐service‐group: default_tx_group
# TC注册中心配置
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: ad50a46c-e62f-4aa6-9ad2-1b1edbaaee03
client:
# tm端配置
tm:
# 一阶段全局提交结果上报TC重试次数 (配置值使用的是默认值)
commit-retry-count: 5
# 一阶段全局回滚结果上报TC重试次数 (配置值使用的是默认值)
rollback-retry-count: 5
# 全局事务超时时间 (配置值使用的是默认值)
default-global-transaction-timeout: 6000
# TM全局事务拦截器顺序 (配置值使用的是默认值)
# 保证拦截器在本地事务拦截器之前执行,也可自定义全局事务和业务开发的拦截器执行顺序
interceptor-order: -2147482648
# 分布式事务降级开关
degrade-check: false
业务代码有@GlobalTransactional
的即为TM
端
RM 端
RM
端和TM
端一致,如果不充当TM
则不需要使用cloud
整合依赖
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.6.0version>
dependency>
两阶段提交协议的演变:
Seata AT
模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如下:
一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql
进行解析,转换成undo_log
,并入库。
二阶段
分布式事务操作成功,则TC
通知RM
异步删除undo_log
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBTpzDN4-1673579586624)(https://typroa-note-images.oss-cn-beijing.aliyuncs.com/img/clipboard%20(1)].png)
分布式事务操作失败,TM
向TC
发送回滚请求,RM
收到协调器TC
发来的回滚请求,通过 XID
和 Branch ID
找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL
并执行,以完成分支的回滚。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iNVDryyE-1673579586625)(https://typroa-note-images.oss-cn-beijing.aliyuncs.com/img/clipboard%20(2)].png)
TM
发起全局事务开启请求TC
,TC
返回XID
,并构建全局事务信息存储到global_table
表中。TM
端执行业务方法,并将XID
向下游远程调用传递。RM
端执行自身的事务方法,并记录到seata
的undo_log
中。并向TC
提交一阶段的分支事务记录TM
发起全局事务提交请求TC
。
TC
释放全局锁,删除对应的全局锁记录,更新全局事务状态。TC
向各分支事务发起异步二阶段分支事务提交。RM
异步提交删除之前的undo_log
日志任务到队列,返回二阶段提交完成状态。TM
发起全局事务回滚请求TC
。
TC
修改全局事务状态:Begin
—>Rollbacking
TC
向各分支事务发起远程调用,通知RM
删除对应undo_log
RM
收到通知,校验undo_log
(数据的前后镜像对比)
TC
收到各分支事务的响应,返回全局事务状态给TM
TM
收到TC
响应的全局事务状态,若则失败根据事务状态进行处理按照上面的 Client
端搭建即可。AT
模式对业务代码几乎无侵入,仅需要在对应的TM
端加上@GlobalTransactional
注解即可。
Seata
提供了 FailureHandler
可扩展接口,可以让开发自行处理一些提交或回滚失败后的处理。
作用于 TM
端,利用各分支事务二阶段处理结果返回给TC
,TC
再将二阶段事务执行结果返回给TM
。
import io.seata.tm.api.DefaultFailureHandlerImpl;
import io.seata.tm.api.FailureHandler;
import io.seata.tm.api.GlobalTransaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class SeataFailureHandlerConfig {
@Bean
public FailureHandler failureHandler(){
return new EmailSeataFailureHandler();
}
class EmailSeataFailureHandler extends DefaultFailureHandlerImpl {
@Override
public void onBeginFailure(GlobalTransaction tx, Throwable cause) {
super.onBeginFailure(tx, cause);
log.warn("邮件通知:分布式事物出现异常:[onBeginFailure],xid:[{}]", tx.getXid());
}
@Override
public void onCommitFailure(GlobalTransaction tx, Throwable cause) {
super.onCommitFailure(tx, cause);
log.warn("邮件通知:分布式事物出现异常:[onCommitFailure],xid:[{}]", tx.getXid());
}
@Override
public void onRollbackFailure(GlobalTransaction tx, Throwable originalException) {
super.onRollbackFailure(tx, originalException);
log.warn("邮件通知:分布式事物出现异常:[onRollbackFailure],xid:[{}]", tx.getXid());
}
@Override
public void onRollbackRetrying(GlobalTransaction tx, Throwable originalException) {
super.onRollbackRetrying(tx, originalException);
log.warn("邮件通知:分布式事物出现异常:[onRollbackRetrying],xid:[{}]", tx.getXid());
}
}
}
AT
模式的回滚利用的是undo_log
日志,所以如果在undo_log
生成后,回滚前,这条数据被其他业务或人为所修改,则无法进行回滚(前后镜像不一致);且全局事务锁不会释放,那么意味着这条业务线在数据修正前将一直无法使用。
解决方案
FailureHandler
进行邮件、短信等通知人为及时处理RM
端涉及业务数据被其他业务修改RM
必须被多个业务调用,则可以在该业务方法上使用@GlobalTransactional
注解进行全局事务锁控制,这样就控制业务逐一执行,避免脏数据产生。在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
详见官网介绍
AT
的前提是支持ACID
的关系型数据库。XA
的前提是支持XA
事务的数据库。XA
模式的分支事务会一直等待TM
向TC
响应执行结果,再进行回滚或提交,在这期间由事务产生的锁是一直占用资源的。AT
模式则是异步化的,根据undo_log
进行回滚。XA
的分支事务注册由TC
统一生成的,所以 XA
模式分支注册的时机需要在 XA start
之前(未来也许会向AT
模式一样)。AT
模式则是在本地事务提交之前才注册分支,可以避免分支执行失败的情况下进行无意义的分支注册。XA
模式代码需要利用本地事务@Transactional
注解,否则方法内的本地事务执行sql
会有事务冲突,造成死锁相比较 AT
模式需要做的改动仅有两点:
TM
端需要加上Spring
的@Transactional
注解@Override
@Transactional
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo, Boolean hasException) {
log.info("=============用户下单=================");
log.info("当前 XID: {}", RootContext.getXID());
// ...
}
yml
配置文件中指定数据源代理模式为 XA
# seata配置需保持和server端一致
seata:
application‐id: ${spring.application.name}
# 数据源代理模式 默认AT
data-source-proxy-mode: XA
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx‐service‐group: default_tx_group
TCC
模式,不依赖于底层数据资源的事务支持。它是一种手动控制的模式,可以应用在各种数据库中,例:Redis
等。
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
prepare
行为commit
或 rollback
行为TCC 模式不依赖于底层数据资源的事务支持:
prepare
行为:调用自定义的 prepare
逻辑。commit
行为:调用自定义的 commit
逻辑。rollback
行为:调用自定义的 rollback
逻辑。TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题。
Seata 的做法是在客户端新增一个 TCC 事务控制表( tcc_fence_log ),里面记录了分支事务一阶段操作的 XID、BranchId 及执行状态等,后续会根据这个记录处理各异常。
空回滚
空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。
产生原因
如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机、网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。
解决
根据 tcc_fence_log 的记录,在执行 Cancel / Rollback 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
幂等
幂等问题指的是 TC 重复进行二阶段提交,因此 Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。
产生原因
如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。
解决
根据 tcc_fence_log 记录状态的字段 status,该字段有 4 个值,分别为:
二阶段 Confirm / Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
悬挂
悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。
产生原因
如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
解决
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行。
AT
等模式需要依赖数据库的事务特性TCC
模式则是通过调用自定义的逻辑进行事务控制AT
等模式对业务的侵入几乎为0
TCC
模式由于是调用自定义的逻辑,所以对业务有较大的的侵入AT
模式利用了undo_log
的镜像记录进行自动回滚处理TCC
模式调用自定义的回滚逻辑,不过针对一些特殊的异常利用了tcc_fence_log
记录表TCC
模式与之前两种的模式实现上有较大的区别,主要在于其是通过调用自定义的逻辑去实现事务控制
客户端建立 tcc_fence_log 表
TCC
模式对一些特殊的异常处理依赖于tcc_fence_log
表,所以需要在各客户端建立该表。(若不考虑这些特殊异常可以不使用)
-- 创建 tcc_fence_log 表,支持 tcc 解决空回滚、悬挂、幂等问题
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;
配置文件中也可以指定表名
# seata配置需保持和server端一致
seata:
application‐id: ${spring.application.name}
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx‐service‐group: default_tx_group
# tcc 配置
tcc:
fence:
# 表名配置
log-table-name: tcc_fence_log
TCC 接口编写
TCC
模式依赖于自定义逻辑,所以需要自己去定义一阶段的预提交,二阶段的回滚/提交方法
注意: commit
和 rollback
方法必须有BusinessActionContext
,否则无法获取到上下文数据(除非不需要利用上下文数据)
import cn.zh.order.domain.vo.OrderVo;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* @author zh
* OrderService对应的Tcc接口
*/
@LocalTCC
public interface OrderServiceSeataTcc {
/**
* TCC的try方法:保存订单信息,状态为支付中
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
* useTCCFence seata1.5.1的新特性,用于解决TCC幂等,悬挂,空回滚问题,需增加日志表tcc_fence_log
*
* @param orderVo
* @return
*/
@TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
Integer prepareSaveOrder(OrderVo orderVo);
/**
*
* TCC的confirm方法:订单状态改为支付成功
*
* 二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
void commit(BusinessActionContext actionContext);
/**
* TCC的cancel方法:订单状态改为支付失败
* 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
注解 / 参数说明
@LocalTCC
声明TCC
接口Bean
。适用于SpringCloud+Feign
模式下,@LocalTCC
需要注解在接口上,此接口可以是寻常的业务接口(并不需要一定独立),只要实现了TCC
的两阶段提交对应方法便可。
@TwoPhaseBusinessAction
作用于一阶段预提交方法,常用参数:
name
:当前tcc
方法的bean
名称,需保证全局唯一commitMethod
:指向当前接口中的提交方法,默认方法名 commit
rollbackMethod
:指向当前接口的回滚方法,默认方法名 rollback
定义完三个方法后,seata
会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。
@BusinessActionContextParameter
作用于预提交方法的参数上,被注解的参数会被传入 BusinessActionContext
(TCC
的业务上下文),这个类会被自动传递到另外两个方法
BusinessActionContext
TCC
的业务上下文,内部利用Map
结构对数据存储。
public class BusinessActionContext implements Serializable {
private static final long serialVersionUID = 6539226288677737991L;
private String xid;
private String branchId;
private String actionName;
private Boolean isDelayReport;
private Boolean isUpdated;
private Map<String, Object> actionContext;
// ...
}
此外数据传递是利用了BusinessActionContextUtil
,这个工具类利用ThreadLocal
对BusinessActionContext
进行数据传递,所以我们也可不使用@BusinessActionContextParameter
,而直接使用BusinessActionContextUtil
进行参数传递
public final class BusinessActionContextUtil {
private BusinessActionContextUtil() {
}
private static final Logger LOGGER = LoggerFactory.getLogger(BusinessActionContextUtil.class);
// ThreadLocal
private static final ThreadLocal<BusinessActionContext> CONTEXT_HOLDER = new ThreadLocal<>();
public static boolean addContext(String key, Object value) {
if (value == null) {
return false;
}
Map<String, Object> newContext = Collections.singletonMap(key, value);
return addContext(newContext);
}
// ...
}
// 例子
@Override
public Integer prepareSaveOrder(OrderVo orderVo) {
// 保存订单
Order order = new Order();
order.setUserId(orderVo.getUserId());
order.setCommodityCode(orderVo.getCommodityCode());
order.setCount(orderVo.getCount());
order.setMoney(orderVo.getMoney());
order.setStatus(OrderStatus.INIT.getValue());
Integer saveOrderRecord = orderMapper.insert(order);
log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
// 内部添加上下文数据
BusinessActionContextUtil.addContext("orderId", order.getId());
return saveOrderRecord;
}
TM 端方式使用 @GlobalTransactional 注解
@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public void saveOrder(OrderVo orderVo, Boolean hasException) {
// ...
}
Saga
模式通常适用于业务流程长或多的场景。
这里不过多介绍,详见官网:https://seata.io/zh-cn/docs/user/saga.html
Seata
设计认为二阶段应处于最终一致性状态,当不一致时理应就不断的去重试直到成功,并且二阶段并不应该考虑各种异常情况,异常应当只会有数据库连接不上这种情况。针对这种情况做法应该是监控系统及时报警然后及时恢复数据库的连接。
对于TCC
模式二阶段的处理不应该有太多的复杂逻辑存在;若是程序的编码错误,不应该考虑在内。
对于AT
模式由于其二阶段回滚依赖于undo_log
镜像所以提供了失败异常处理。
在TCC
模式中如果二阶段出现异常,则会每秒一次去不断重试(1.6.0
,后续或会增加重试间隔配置),即使服务重启也会去不断重试;这是根据server
端记录的事务状态去驱动执行客户端二阶段方法,在server
端branch_table
中会记录此次的上下文信息,并传递执行二阶段。
gitee:https://gitee.com/ahang-gitee/learn-seata