Seata 分布式事务
分布式事务介绍
当我们使用单体应用的时候,我们的多个业务都在同一个应用中运行,当某一业务被执行后,这个业务可能会涉及到多个协助业务功能,我们可以轻松地对这些协助的业务功能进行事务管理。
但如果我们使用的是分布式系统呢?分布式系统使用的多个独立的服务,通过Fiegn网络请求的方式协助处理业务的,那又该如何解决事务管理的问题呢?
分布式服务案例
微服务下单业务,在下单时会调用订单服务,创建订单并写入数据库。然后订单服务调用账户服务和库存服务:
- 账户服务负责扣减用户余额
- 库存服务负责扣减商品库存
用户下通过计单服务下单,计单服务在数据库中创建订单后,使用Fiegn调用账户服务和库存服务进行相应的扣款扣库存操作。
出现问题
但存在这样一个问题,万一用户在下单时,订单服务创建订单成功能,扣款成功了,却库存不足,这全导致出现事务不一致的问题:订单创建了,钱扣了,库存没扣。
CAP定理
CAP定理是1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
CAP定理中,只能俩俩满足,却无法同时三个满足。
CAP定理-Consistency一致性
Consistency(一致性):指的是用户访问分布式系统中的任意节点,得到的数据必须一致
CAP定理-Availability可用性
Availability (可用性):指的是用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
CAP定理-Partition tolerance分区容错
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
CAP之间的关系
CP: 一致性与分区容错
- 当Partition tolerance分区容错时,也就是即使服务器之间发生了网络断开,但它们依然要求提供服务
- 但网络都断开了,服务器要想访问的数据一致,则断开同步的服务器只能等待网络恢复后才能实现,在此之前,服务器的数据是不可用了。不同步的服务器只能被暂时剔除。所以CP无法兼容A (Availability可用性)
- 通俗讲:又要数据一致又要容错网络丢失,那只能去除不可用的服务了
CA: 一致性与可用性
- 如果要求满足任意服务器的数据必须同步,且可以即时访问,那必然不能发生网络同步断开
- 因此,CA不能兼容P (Partition tolerance分区容错)
- 通俗讲:又要数据一致又要所有服务都能用,那就必须保证不能出现网络断开。
AP: 可用性与分区容错
- 如果服务器同步发生断开时,又要求必须能访问所有服务器,那必然它们之间的数据,就不可能保证一致性
- 因此,AP不能兼容C(Consistency一致性)
- 通俗讲:又要所有都能用,又要允许网络断开,那必须数据就不一致。
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务)。
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务
Seata 基础
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
架构
Seata事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
搭建TC服务
TC服务是一个协调者,它会管理多个微服务之间的操作事务,所有的服务调会先向TC中进行核对,核对成功后才会下发给RM让各微服务进行最终操作,是强一致性CP模型的基础实现
1.下载
首先我们要下载seata-server包,地址在http://seata.io/zh-cn/blog/download.html
2.解压
3.修改配置
修改conf目录下的registry文件,用于把服务注册到nacos中
参考配置内容
registry {
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
4.在nacos添加配置
特别注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。
参考配置如下
# 数据存储方式,db代表数据库
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&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
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
5.创建数据库表
别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。
新建一个名为seata的数据库,运行课前资料提供的sql文件:
SQL代码如下:
这些表主要记录全局事务、分支事务、全局锁信息:
/*
Navicat Premium Data Transfer
Source Server : local
Source Server Type : MySQL
Source Server Version : 50622
Source Host : localhost:3306
Source Schema : seata_demo
Target Server Type : MySQL
Target Server Version : 50622
File Encoding : 65001
Date: 20/06/2021 12:38:37
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of branch_table
-- ----------------------------
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of global_table
-- ----------------------------
-- ----------------------------
-- Records of lock_table
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
6.启动TC服务
进入bin目录,运行其中的seata-server.bat即可:
开浏览器,访问nacos地址:http://localhost:8848,然后进入服务列表页面,可以看到seata-tc-server的信息:
微服务集成seata
通过图中可知,我们的微服务中需要都搭载一个RM用于管理每一个请求的事务,本节装在微服务中集成seata
1.引入依赖
我们需要在微服务中引入seata依赖:
<!-- spring-cloud-starter-alibaba-seata 是带有1.3.0版本seata的带有自动配置的依赖,但我不希望使用1.3.0因为太老了 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 然后另外引入一个新一点的seata版本 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
2.修改配置文件
在微服务中配置seata在nacos中注册发现
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
# 参考tc服务自己的registry.conf中的配置
type: nacos
nacos: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server # tc服务在nacos中的服务名称
cluster: SH
tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: SH # 集群名称
注意:
XA 模式
XA 规范是 X/Open 组织定义的分布式事务处理(DTP)标准,XA 规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对XA规范提供了支持
可以看出,XA 模式通俗来讲就是,所有涉及到调用的服务,都会先预先执行,但并不做最终的提交,而是先集中告诉Seata,当Seata接收所有的服务都执行成功时,才会下发提交通知给到服务,是一种强一致性模式,因为会等待所有服务完成任务,因此它的性能相对较低。
Seata 中的 XA 模式实现
Seata 中的 XA 模式做了一些调整,多了TM:
TM 是用于管理全局事务的服务。
使用Seata的分布式事务:
1.修改application.yaml文件中的seata配置,使它开启为XA模式
seata:
data-source-proxy-mode: XA # 开启数据源代理的XA模式
2.在需要处理业务逻辑的方法上使用注解【@GlobalTransactional】即可。
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
// 扣余额 ...略
// 扣减库存 ...略
return order.getId();
}
在调用的数据库操作或Fiegn请求过种中发生异常时,所有的操作都会被回滚。
AT 模式
AT模式同样是分阶段提交的事务模型,不过却弥补了XA模型中资源锁定周期过长的缺陷
模式说明:
TM向每个服务的RM发送可操作通知,然后RM所在的每个服务都开始进行操作,操作之前会把当前状态数据保存为一个快照。
操作完成后会直接提交,并报告操作是否成功的状态给TC
- 如果所有服务都操作成功,则TC会向各RM报告【提交】通知,因为服务在操作时就直接提交了,所以RM收到后只会删除快照
- 如果服务中存在操作失败的,则TC会向各RM报告【回滚】通知,各服务会利用之前生成的快照进行回滚恢复操作,恢复完成后删除快照。
AT 模式的脏写问题
AT 模式使用的是记录快照,先提交,后回滚的方案,这就引出了一个问题:因为数据发生了提交,那么数据库锁必定会被释放,此时后来的事务就可以自由操作数据了。而这时,之前的操作发生了回滚,快照的数据会覆盖后来的事务所做的数据操作。
事件重现图解:
图一解析:
- 事务1对数据进行操作并马上提交,记录了快照100,数据表中的数据从100改为90
- 事务1完成后就释放DB锁了
图二解析:
- 事务2获取DB锁,开始对数据进行写操作,记录快照为90,并改写数据为80
- 事务2完成写操作后,释放了DB锁
图三解析:
- 此时事务1发现服务有误,需要对原先的操作进行回滚,这时会拿到之前记录的快照100,恢复到数据表中
- 数据表中的数据改回100
- 发生脏写的问题。
AT 模式脏写问题的Seata解决方案
Seata为了解决AT模式下脏写的问题,为AT模式引入了【全局锁】的概念。
图一解析:
- Seate引入了全局锁的概念(全局锁由Seata管理)
图二解析:
- 事务1对数据表进行写操作并提交(记录快照),完成后释放DB锁,然后在全局锁中记录下该表的锁信息(锁信息会在删除快照后删除)
图三解析:
- 此时事务2获取DB锁,并准备对数据进行写操作
- 但此时事务2获取到全局锁中包含了该数据表字段的锁信息,于是开始等待锁信息的删除
- 如果此时事务1的操作成功完成,对锁信息和快照删除,那么事务2将开始对数据表进行写操作
图四解析:
- 此时如果事务1的操作有误,需要回滚,那么事务1会获取DB锁进行数据回滚操作
- 但是DB锁已被事务2获取了,事务1无法获取DB锁,于是等待DB锁的释放
- 与此同时,事务2也因为等待全局锁的删除而不停地等待不进行释放
- 这时发生了锁的循环等待
图五解析:
- 事务2的等待全局锁删除具有超时限制,会比事务1的等待DB锁更快到期,因而是事务2会先超时释放DB锁
- 事务1获取到DB锁,并对数据表进行回滚操作,并删除全局锁与快照。
- 实现解决脏写问题。
AT 模式全局锁的问题
因为AT 模式下的全局锁,是由Seata进行管理的,能使用全局锁的事务,必须在Seata中的事务才有效,若此时存在非Seata事务管理操作的数据库(如Spring-tx),依然存在脏写问题。
图一解析:
- 事务1对数据进行快照,并写数据后马上提交,并设置全局锁,释放DB锁
图二解析:
- 此时,非Seata事务对数据表进行了写操作,并提交后,释放DB锁
- 若此时事务1发现服务有误,需要回滚,此时会获取DB锁,但此时非Seata事务正在写数据,因此事务1进行等待DB锁。
图三解析:
- 事务1获取到DB锁,并根据快照进行恢复
- 如果事务1直接使用快照进行怀复,那么就会使非Seata的事务2发生脏写
图四解析:
- 实际上Seata事务会在操作数据之前,和操作数据之后,都分别生成快照,分别为操作前快照与操作后快照
- 若此时Seata对数据进行快照恢复时,发现当前数据表中的数据,和Seata事务操作后的快照数据不一致,那么Seata认为,在它释放DB锁到回滚之前,数据表发生非Seata事务的操作。
- Seata会直接报出错误,需要人工介入。
实现AT模式事务
1.因为AT模式使用的是全局锁,我们的全局锁设置存放在数据库中,因此我们需要创建用于保存全局锁的数据表到TC服务的数据库中:
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
2.AT模式中,会存在快照,我们同样把快照存放在数据库中,而快照则是由每个服务所自行操作的,所以快照应当存放在每个服务的数据库中:
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
3.修改 application.yaml 文件中的seata事务模式为AT
seata:
data-source-proxy-mode: AT # 开启数据源代理的AT模式
4.微服务会在操作时自动把快照写到undo_log表中(操作前与操作后的快照都写到这个表),而全局锁会在TC服务器的数据库中保存。
TCC 模式
TCC 与 AT 模式同属相同的最终一致,过程软状态的模式
每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
TCC工作原理图:
TCC 事务实现
概念:
1.我们要创建一个接口,声明三个方法,用以表示 Try、confirm、cancel 的执行机制。
2.在Try方法中,对数据进行写入,并对数据做一个预留,存放在freeze表中
3.confirm方法表示已成功完成数据调用,所以可以对freeze表中预留删除
4.如果数据调用不成功,则调用cancel方法,对数据进行恢复,mapper.refund
5.对cancel方法做空回滚判断
6.对try方法做业务悬挂判断
预留表freeze的内容示例:
1.包含xid(由TCC事务提供)
2.包含操作用户userid
3.包含改要写数据
4.包含当前状态(try,confirm,cancel)
/*
Navicat Premium Data Transfer
Source Server : local
Source Server Type : MySQL
Source Server Version : 50622
Source Host : localhost:3306
Source Schema : seata_demo
Target Server Type : MySQL
Target Server Version : 50622
File Encoding : 65001
Date: 23/06/2021 16:23:20
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for account_freeze_tbl
-- ----------------------------
DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
`state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
-- ----------------------------
-- Records of account_freeze_tbl
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
步骤:
1.创建一个接口,分别表示try、confirm、cancel方法。接口上方使用注解【@LocalTCC】开启TCC事务模式
2.try方法名不是名为【try】,它是一个概念,try方法需要我们自己定义,通常来说,try方法都是真实的业务操作方法。使用注解【@TwoPhaseBusinessAction】来定义该方法为try方法,并添加 name = "prepare", commitMethod = "", rollbackMethod = "" 三个属性分别表示【try方法的方法名】、【confirm方法的方法名】、【cancel方法的方法名】
@LocalTCC
public interface TCCService {
/**
* Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
*/
@TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")
void prepare(@BusinessActionContextParameter(paramName = "param") String param);
/**
* 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
*
* @param context 上下文,可以传递try方法的参数
* @return boolean 执行是否成功
*/
boolean confirm (BusinessActionContext context);
/**
* 二阶段回滚方法,要保证与rollbackMethod一致
*/
boolean cancel (BusinessActionContext context);
3.对这个接口做一个实现,包括创建用于预留的freeze表格的mapper
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* TCC事务接口
*/
@LocalTCC
public interface AccountTCCService {
/**
* 定义 try 方法
* try 方法通常都是真实的操作数据库的逻辑
* 只是添加了一些TCC业务,如插入预留信息,和防业务悬挂的问题
*/
@TwoPhaseBusinessAction(name = "dosomething",commitMethod = "confirm",rollbackMethod = "cancel")
void doSomething(Long id);
/**
* confirm 提交的方法
* 通常这个方法是用于删除预留信息的
* @param ctx
* @return
*/
boolean confirm(BusinessActionContext ctx);
/**
* cancel 回滚的方法
* 需要判断是否存在空回滚,如果有,则需要插入一条已回滚的记录
* 否则做回滚
* @param ctx
* @return
*/
boolean cancel(BusinessActionContext ctx);
}
4.实现方法(拟代码,实际问题请实际思考代码执行)
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
// 操作用户数据的表
@Resource
private AccountMapper accountMapper;
// 操作预留数据表
@Resource
private AccountMapper freezeMapper;
/**
* 1.对业务进行写操作
* 2.预留数据到freeze表中
* @param id
*/
@Override
@Transactional
public void doSomething(Long id, int money) {
String xid = RootContext.getXID();
accountMapper.doThing();
AccountFreeze freeze = new AccountFreeze();
// 增加 freeze 中的数据
freeze.setUserId(id);
freeze.setFreezeMoney(money);
freeze.setState(try);
freeze.setXid(xid);
accountFreeze.insert(freeze);
}
/**
* 对于确认提交,我们只需要删除freeze表中的预留数据就可以
* @param ctx
* @return
*/
@Override
public boolean confirm(BusinessActionContext ctx) {
String xid = ctx.getXid();
int count = freezeMapper.deleteById(xid);
return count == 1;
}
/**
* 回滚操作,把写好的数据改回去
* @param ctx
* @return
*/
@Override
public boolean cancel(BusinessActionContext ctx) {
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
// 先查询预留表中的数据
AccountFreeze freeze = freezeMapper.selectById(xid);
// 对数据恢复
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 改状态为 Cancel
freeze.setFreezeMoney(0);
freeze.setState(Cancel);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
TCC的空回滚和业务悬挂
TCC的空回滚是指:若TM调用分支服务时发生阻塞或延迟超时时,会向TC报告失败通知,TC向RM发送回滚通知,而阻塞的服务在未执行Try之准,Cancel回滚就发生了,Cancel需要判断Try是否有执行过,如果没有,Cancel应当执行空回滚
TCC的业务悬挂是指:若TM调用分支服务时发生的阻塞致使TC通知RM回滚发生。而发生后,阻塞解除,Try继续执行,但此时事务早已结束,这使得Try在事务结束后才来执行,这种称为业务悬挂,我们需要在Try执行之前判断数据表中是否有Cancel状态的记录。
解决空回滚与业务悬挂
空回滚:
我们应当在Cancel方法中,在修改回滚表之前,查询是否存在对应 xid 的记录,如果没有对应的 xid 的记录,说明 Try 没有被执行, Cancel 应当做空回滚
拟代码:
/**
* 回滚操作,把写好的数据改回去
* @param ctx
* @return
*/
@Override
public boolean cancel(BusinessActionContext ctx) {
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
// 先查询预留表中的数据
AccountFreeze freeze = freezeMapper.selectById(xid);
/**
* 如果数据表中不存在数据,则表示try未进行操作
* 这时我们需要创建一个freeze,状态为Cancel的记录
*/
if (freeze == null){
// 改状态为 Cancel
freeze.setFreezeMoney(0);
freeze.setXid(xid);
freeze.setState(Cancel);
freezeMapper.insert(freeze);
return true;
}
/**
* 幂等性判断
* 对于可能重试进入的操作,我们应该进行阻止
*
*/
if (freeze.getState() == Cancel){
// 已经处理过一次了,所以直接返回就可以
return true;
}
// 对数据恢复
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 改状态为 Cancel
freeze.setFreezeMoney(0);
freeze.setState(Cancel);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
业务悬挂:
我们在Try方法中判断是业务是否已经曾经回滚过了,判断的基础是freeze表中是否包含了对应Xid的记录状态为Cancel的,如果存在,则说明事务已经完成了回滚,这时业务就不能再做了。
/**
* 1.对业务进行写操作
* 2.预留数据到freeze表中
* @param id
*/
@Override
@Transactional
public void doSomething(Long id, int money) {
String xid = RootContext.getXID();
accountMapper.doThing();
// 查询数据表中是否有记录,如果有记录,说明cancel已经空回滚过
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze != null){
return;
}
AccountFreeze freeze = new AccountFreeze();
// 增加 freeze 中的数据
freeze.setUserId(id);
freeze.setFreezeMoney(money);
freeze.setState(try);
freeze.setXid(xid);
accountFreeze.insert(freeze);
}
SAGA 模式(略)
SAGA 模式是 Seata提供的长事务解决方案,分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做,失败则能过编写补偿业务来回滚。
Saga模式优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
四种模式对比
XA | AT | TCC | SAGA | |
一致性 | 强一致 | 弱一致 | 弱一致 | 最终一致 |
隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
代码侵入 | 无 | 无 | 有,要编写三个接口 | 有,要编写状态机和补偿业务 |
性能 | 差 | 好 | 非常好 | 非常好 |
场景 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式事务场景都可以 | •对性能要求较高的事务。
•有非关系型数据库要参与的事务。 |
•业务流程长、业务流程多
•参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口 |
Seata 高可用(略)
共有 0 条评论