分布式事务Seata实践
# 理论篇
# 什么是事务
关于事务我们一定会想到下面这四大特性:
- 原子性:所有操作要么全都完成,要么全都失败。
- 一致性: 保证数据库中的完整性约束和声明性约束。
- 隔离性:对统一资源的操作不会同时发生的。
- 持久性:对事务完成的操作最终会持久化到数据库中。
# 分布式事务简介
而分布式事务,不仅包含上述四个特性,这个事务可能是跨服务也可能是跨数据源的一种事务。
# CAP原则
CAP我们可以将其拆分为3个字母,对应中文含义分别是:
- C(Consistency):一致性。使用相同条件到集群中的节点获取数据都是一致的,当我们将任意节点提交修改,其他节点也会完成数据同步。
2. A(Availability):可用性。用户访问集群中任意一个健康节点都会得到响应,而不是像报错或者响应超时。
3. P(Partition tolerance):容错性。集群中的节点可能会因为各种原因和集群失去联系(例如当前节点和集群通信的端口号被运维不小心关闭了),但是这些节点还是可以正常对外提供服务,这种分区了还能对外部提供的服务的特性就叫做分区容错性。

为什么CAP在符合分区容错性(P)的基础上只能在一致性(C)和可用性(A)中选择一个特性呢?
由上我们可以知道P肯定是可以满足的,如果我们需要满足C(一致性),那么集群断开连接时,我们必须等待各个节点再次连接并完成数据同步后才能对外提供服务,那么在一定时间内,服务是不可用的。如果我们要满足A(可用性),那么一旦集群节点断开连接后不管数据是否同步都要对外提供服务,这样一来C(一致性)就不一定可以满足。
这就是为什么CAP只能符合CP或者AP。
# 什么是BASE理论
而CAP一种解决理论则是BASE理论,即:
Basically Available (基本可用):允许分布式事务运行过程中损失部分可用性,即保证核心可用。Soft State(软状态):在数据更新时,允许某一小段时间数据不一致。Eventually Consistent(最终一致性):虽然无法保证强一致性,但是软状态结束后,还是可以保证最终数据是一致性的。
# 什么是Seata
Seata是一款开源的分布式事务解决方案。Seata这个开源工具实现分布式事务则是基于以下三个角色:
# Seata的三大角色
TC (Transaction Coordinator)事务协调者:维护全局事务和分支事务的状态,协调全局事务的提交和回滚。TM (Transaction Manager) 事务管理器:定义全局事务的范围,开始全局事务、提交或者回滚全局事务。RM (Resource Manager) 资源管理器:管理分支事务处理的资源,并向TC注册分支事务以及报告分支事务的状态,并驱动分支事务的提交和回滚。
上述的概念可能读者不态度,我们不妨以下图为例子讲述一下这三大角色的工作流程,当我们创建的一个分布式全局事务时,TM就划定了这个事务的范围,见下图蓝色部分。 然后所有的RM都会执行本地事务,所谓本地事务我们可以通俗的理解为JDBC操作SQL,在此期间Seata会记录sql修改前后的镜像到一张名为undo-log的表中,当所有RM都提交成功之后,TM就会驱动TC发起全局事务提交,反之发起全局事务回滚。

# 实践篇
# 部署Seata并注册到Nacos上
首先我们来部署以下Seata,首先我们得去官网下载一下资源,以笔者为例,笔者当前使用的是1.4.2这个版本:
https://github.com/seata/seata/releases (opens new window)

完成后我们就可以开始Seata的配置工作了,首先我们先配置conf目录下的registry.conf,读者可以参见以下注释自行修改。这里需要留意一下笔者下面配置中cluster指定名称为SH,这个配置后续服务注册到Seata中会用到。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
# 服务注册后的名称
application = "seata-server"
# nacos地址
serverAddr = "127.0.0.1:8848"
# nacos组名
group = "myGroup"
# nacos使用命名空间
namespace = "dev"
# 使用集群的节点名称
cluster = "SH"
# nacos账户和密码
username = "nacos"
password = "nacos"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意为了让tc的集群可以共享配置,我们可以将服务端配置内容配置到nacos上。如下图所示

可以看到笔者在dev命名空间的myGroup下,添加了下面这样一段配置,读者只需按照下面的模板修改数据库相关的配置即可
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 读者只需修改下面这些配置即可
store.db.url=数据库地址
store.db.user=用户名
store.db.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
# 事务、日志等配置
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Seata管理全局事务和分支事务也是采用SQL的形式,所以我们需要使用下面这段SQL脚本建立全局事务和分支事务数据表。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
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;
-- ----------------------------
-- 全局事务表
-- ----------------------------
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;
SET FOREIGN_KEY_CHECKS = 1;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
完成这些步骤直接启动Seata即可,以笔者为例,因为笔者用的是windows系统,所以运行bin目录下的seata-server.bat。
若显示出下面这样一段输出,则说明启动Seata成功了

# 在服务中集成Seata
首先在pom中引入下面这段依赖,可以看到由于版本的原因,笔者手动讲spring-cloud-starter-alibaba-seata中自带的seata-starter排除,手动引入了1.4.2版本的seata。
<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 starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后在application.yml中添加下面这样一段配置,具体含义参加注释
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "dev" # namespace,默认为空
group: myGroup # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: SH
data-source-proxy-mode: XA
2
3
4
5
6
7
8
9
10
11
12
13
14
15

每个服务都完成上述配置之后,启动时就会用到Seata了。
# 基于Seata实现分布式事务的四种方式
# XA模式
# 了解XA模式的工作原理和优缺点
如下图,XA模式的分布式事务执行流程大抵如下,简而言之,我们可以将其分为两个阶段,首先是:
RM第一阶段:所有的分支事务向TC注册自己的状态,完成后各自执行SQL但是不提交,并将状态报告给TC。
TC 第二阶段:
- TC查看当前事务的所有分支事务是否都成功了,如果都成功则协通知所有RM提交事务,反之回滚事务。
- 收到TC全局事务提交后,RM将自己管理的分支事务也提交了。

XA模式的分布式事务优缺点:
- 优点: 强一致性,符合
ACID原则。 且实现简单,没有代码侵入。 - 缺点:为了保证强一致性,所以必须保证所有
SQL执行没有问题才能提交,所以一阶段这些数据会被锁住,导致其他需要执行这些SQL的事务被阻塞,性能较差。
# 实践
注意为了保证后续执行不报错,建议将pom中的MySQL驱动版本改为8.0.11
首先修改每个需要使用XA模式的服务,在yml添加下面这样一段配置
seata:
data-source-proxy-mode: XA
2
XA模式代码如下所示,可以看到笔者仅仅是在方法上加一个GlobalTransactional注解就能保证服务1和服务2之间分布式事务的ACID。感兴趣的读者可以自行编写一个demo,可以看到一旦任意服务报错,控制台就会输出RollBack将事务操作回滚。
@GetMapping("/test")
@GlobalTransactional
public String hello() {
UserDto userDto=new UserDto();
userDto.setLoginName("admin123");
userDto.setName("admin123");
userDto.setPassword("456");
userDto.setImageCode("456");
userDto.setImageCodeToken("456");
// 服务1的保存
userService.save(userDto);
//服务2的数据操作
fileService.hello();
return "123";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# AT模式
AT模式的工作流程图如下所示,总结以下它的工作流程:
RM第一阶段工作
- 注册分支事务。
- 解析
SQL记录修改前后的SQL镜像并存储到undo-log。 - 执行业务
SQL并直接提交事务。 - 通知
TC当前事务的状态。
RM第二阶段工作
- 如果
TC确定所有事务都成功且发起通知告知当前RM,则RM会将undo-log删除。 - 如果
TC通知失败则RM会根据undo-log将数据还原。

# AT模式优缺点以及和XA模式的区别
先来说说和XA模式的区别吧:
- XA一阶段会锁定资源,AT模式则是直接提交事务不锁定资源。
- XA模式回滚依赖数据库事务,AT模式则是根据我们上面自己创建的undo-log的内容进行还原。
- XA模式是强一致性,AT模式是最终一致性。
AT模式优缺点:
- 一阶段提交不会锁定资源,性能较好。
- 利用全局锁实现读写隔离。
- 没有代码侵入,便于使用。
缺点:
- 框架记录
undo-log会有一定开销,但是性能相比XA会好很多。 - 两阶段属于软阶段,属于最终一致性,中间可能会有数据不一致问题。
演示,实现AT模式的方法也很简单,首先为了保证线程安全问题,我们需要对seata数据刷入下面这张表,用于全局锁记录。
-- ----------------------------
-- 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
完成后,对着我们服务需要用到的数据库,刷入undo-log表
-- ----------------------------
-- 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;
2
3
4
5
6
7
8
9
10
11
12
13
14
完成后对着yml文件添加下面这样一段配置
seata:
data-source-proxy-mode: AT # 默认就是AT
2
笔者代码如下所示,可以看到服务1进行保存操作,服务进行其他数据库操作,但是笔者在服务2中随意添加了一个报错。
@GetMapping("/test")
@GlobalTransactional
public String hello() {
UserDto userDto=new UserDto();
userDto.setId("1");
userDto.setLoginName("1");
userDto.setName("1");
userDto.setPassword("1");
userDto.setImageCode("1");
userDto.setImageCodeToken("1");
//服务1的保存
userService.save(userDto);
// 服务2随意添加了一个报错
fileService.hello();
return "123";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
经过debug我们可以发现,在服务1完成数据提交后

lock_table记录了一条锁的信息。

undo-log也会记录一条信息,我们不妨点入查看详细内容

查看详细内容可以看到,这里面记录的就是插入前后的数据库的镜像
{
"@class":"io.seata.rm.datasource.undo.BranchUndoLog",
"xid":"192.168.237.1:8091:6989933091681009799",
"branchId":6989933091681009804,
"sqlUndoLogs":[
"java.util.ArrayList",
[
{
"@class":"io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType":"INSERT",
"tableName":"user",
"beforeImage":{
"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
"tableName":"user",
"rows":[
"java.util.ArrayList",
[
]
]
},
"afterImage":{
"@class":"io.seata.rm.datasource.sql.struct.TableRecords",
"tableName":"user",
"rows":[
"java.util.ArrayList",
[
{
"@class":"io.seata.rm.datasource.sql.struct.Row",
"fields":[
"java.util.ArrayList",
[
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"id",
"keyType":"PRIMARY_KEY",
"type":1,
"value":"1"
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"login_name",
"keyType":"NULL",
"type":12,
"value":"1"
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"name",
"keyType":"NULL",
"type":12,
"value":"1"
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"password",
"keyType":"NULL",
"type":1,
"value":"1"
}
]
]
}
]
]
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
一旦报错控制台就会输出一段Rollback的内容,并且事务也会根据undo-log回滚
# TCC模式
TCC模式依旧延续之前的架构,只不过TCC各个阶段都需要人工实现,TCC实现分布式事务我们必须实现以下三个方法:
- try:进行资源检测和预留,例如我们要将user1的name从1改为2,那么我们就得将1这个值预留下来(存到一张资源预留表中),并提交1改为2这个操作,并提交事务。
- confirm:完成资源的业务操作(将预留表中的内容删除),try成功就要求confirm方法逻辑一定要成功。
- cancel:事务提失败,将预留资源释放
(将预留表中的数据状态设置为取消,并拿着这个旧值去还原数据,即手动回滚补偿)。

TCC模式优缺点:
- 一阶段即可完成数据提交,释放数据库资源,性能好。
- 无需像
AT模式那样需要生成快照,使用全局锁,性能最强。 - 不依赖数据库事务,而是手动补偿依赖操作,可以用非事务型数据库。
缺点:
- 有代码侵入,需要手写资源消耗、补偿等逻辑。
- 软状态,属于最终一致性。
- 需要考虑
confirm和cancel的情况,做好幂等相关处理。
为了演示TCC模式,笔者就基于自己的服务演示一下TCC的使用,举个例子,笔者现在有个为system的服务,system服务会将id为1的user名字进行修改,完成后同步修改file服务的相关数据库。

所以笔者就以system为例实现一下TCC相关的服务,我们首先梳理一下思路,首先我们要做的就是将id为1的user的name由1改为2。那么我的try方法就需要进行以下操作:
1. 将user表id为1的用户的旧值冻结起来(这条数据状态设置为0,代表处于try状态),便于后续回滚补偿。
2. 将user表id为1的用户name值更新。
完成后编写confirm方法,因为只有try成功了才会走到confirm方法,它的逻辑很简单,将记录user表冻结的值的数据删除即可。
而cancel方法就是对资源的补偿处理,框架走到cancel则说明事务执行失败了,我们需要将修改的数据进行补偿,例如我们的user表id为1的name由1改为2,那么我们就需要到资源冻结表找到这个条数据的冻结记录,拿着旧值还原user数据,完成后再将这张资源冻结表数据状态设置为3(已取消)
完成上述逻辑后,我们还需要考虑两个问题,第一个是空悬挂问题,如下图,我们的try方法执行太久导致超时,框架自动执行cancel回滚补偿事务,结果try方法再次执行,因为此时资源已经回滚所以也没有try的必要了。
对此我们的要在try方法加上这么一个逻辑,如果资源冻结表关于本次操作的数据状态为3,则直接返回失败。

还有一个就是空回滚问题,即try方法执行过程中直接报错或者长时间阻塞了还没执行到sql逻辑,cancel就已经完成回滚了,结果还是走到了cancel方法,cancel并没有需要回滚的数据。
对此,我们只需要将修改前的值手动存到资源冻结表中并设置状态为已取消,制造一条资源冻结数据直接返回即可。

完成上述分析后,我们就可以进行编码操作了,我们首先需要编写一个@LocalTCC接口,定义try、confirm、cancel方法,可以看到我们用TwoPhaseBusinessAction注解告知框架三大行为用哪个方法,并用BusinessActionContextParameter设置全局参数,confirm和cancel都可以通过BusinessActionContext 获取到这些参数
@LocalTCC
public interface SystemTCCService {
@TwoPhaseBusinessAction(name = "doTry", commitMethod = "confirm", rollbackMethod = "cancel")
void doTry(@BusinessActionContextParameter(paramName = "id") String id,
@BusinessActionContextParameter(paramName = "oldVal") String oldVal,
@BusinessActionContextParameter(paramName = "val") String val);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
完成后,我们继承这个类实现一个service,代码如下可以看到笔者doTry做的就是拿着上下文的oldVal(即user表的旧值)冻结起来存到资源冻结表,并将状态设置为0(代表这条数据处于try状态)。通过xid查询资源冻结表看看是否有数据,若有则说明这是一个空悬挂操作,直接返回。
confirm和cancel逻辑就比较简单了,读者可以自行阅读代码和注释,无非是删除补偿数据或回滚数据并作废资源冻结表数据而已。
@Service
public class SystemTCCServiceImpl implements SystemTCCService {
@Autowired
private UserMapper userMapper;
@Autowired
private SystemFreezeTblMapper systemFreezeTblMapper;
@Override
public void doTry(String id, String oldVal,String val) {
// 0.获取事务id
String xid = RootContext.getXID();
// 业务悬挂判断: 判断freeze中是否有冻结记录,如果有,一定是cancel执行过,拒绝业务操作
if (systemFreezeTblMapper.selectByPrimaryKey(xid) != null) {
// cancel执行过,我要拒绝业务
return;
}
User user = new User();
user.setId(id);
user.setLoginName(val);
user.setName(val);
user.setPassword(val);
userMapper.updateByPrimaryKeySelective(user);
//冻结住原有的值
SystemFreezeTbl systemFreezeTbl = new SystemFreezeTbl();
systemFreezeTbl.setXid(xid);
systemFreezeTbl.setUserId(user.getId());
systemFreezeTbl.setFreezeVal(user.getName());
systemFreezeTbl.setFreezeOldVal(oldVal);
//0 try 1 confim 2 cancel
systemFreezeTbl.setState(0);
systemFreezeTblMapper.insert(systemFreezeTbl);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
//成功则删除冻结的表
String xid = ctx.getXid();
int count = systemFreezeTblMapper.deleteByPrimaryKey(xid);
return count > 0;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查询冻结记录
String xid = ctx.getXid();
// 根据id查询冻结表的记录
SystemFreezeTbl freeze = systemFreezeTblMapper.selectByPrimaryKey(xid);
// 处理空回滚
if (freeze == null) {
//空回滚
freeze = new SystemFreezeTbl();
String userId = ctx.getActionContext("id").toString();
freeze.setUserId(userId);
freeze.setFreezeVal(ctx.getActionContext("val").toString());
freeze.setFreezeOldVal(ctx.getActionContext("oldVal").toString());
freeze.setState(2);
freeze.setXid(xid);
systemFreezeTblMapper.insert(freeze);
return true;
}
// 幂等判断
if (freeze.getState() == 2) {
// 已经处理过了cancel,无需重复
return true;
}
//手动补偿user表,进行数据还原
User user = new User();
user.setId(ctx.getActionContext("id").toString());
user.setName(freeze.getFreezeOldVal());
userMapper.updateByPrimaryKeySelective(user);
//将资源冻结表状态设置为2 代表已取消
freeze.setFreezeVal("");
freeze.setState(2);
int count = systemFreezeTblMapper.updateByPrimaryKey(freeze);
return count > 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
完成system核心服务层代码编写之后,我们也可以照猫画虎的完成file服务的编写,笔者这里就不多做演示了,直接贴出controller的代码。我们可以开着debug模式试着将让fileService报错,调试时就会发现systemTCCService最终会执行cancel完成数据补偿。
@GetMapping("/test")
@GlobalTransactional
public String hello() {
systemTCCService.doTry("1", "1", "2");
fileService.hello();
return "success";
}
2
3
4
5
6
7
8
# SAGA模式
Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。
https://seata.io/zh-cn/docs/user/saga.html (opens new window)
总的来说它和TCC差不多,它也是分为两个阶段:
- 一阶段将修改提交。
- 二阶段则是根据一阶段进行反馈,若是成功则什么都不做,反之进行回滚补偿。
优缺点
优点:
1. 基于事件驱动实现异步调用,性能较好,吞吐高。
2. 一阶段直接提交事务,无锁,性能较好。
3. 无需像TCC一样手动编写不同阶段的方法。
缺点:
1. 软状态时间不确定,时效性较差。
2. 无事务隔离,可能出现脏写的情况。
# 总结优缺点

# 分布式事务使用seata处理呢?为什么不用mq解决分布式事务的问题呢
主要还是考虑兼容性问题,目前主流的消息中间件中rocketMQ支持事务。而且考虑将来可扩展,可能我们还会更换中间件以及兼容多数据库,所以使用seata实现分布式事务是最合适的。 而且消息队列主要作用也并不是用于分布式事务问题,它的主要作用是解耦、异步、削峰。而seata是目前比较主流的分布式事务解决方案。
# 参考文献
微服务事务——Seata: https://juejin.cn/post/7121180233532702734 (opens new window)
深度剖析 Seata TCC 模式(一):https://seata.io/zh-cn/blog/seata-tcc.html (opens new window)
Seata AT模式:https://seata.io/zh-cn/docs/dev/mode/at-mode.html (opens new window)
深入理解Seata流程:https://blog.csdn.net/weixin_39986856/article/details/103630407 (opens new window)