SpringCloud---Seata

(一).介绍Seata

Seata是一款开源的分布式事务 解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata为用户提供了AT TCC SEGA 和 XA事务模式,打造了一站式的分布式解决方案,

在介绍Seata之前,先要介绍一下什么是分布式事务。

(二).介绍分布式事务

1.回顾事务

在之前介绍MYSQL的时候,我们就介绍到过事务的概念。关于数据库事务的概念,指的就是把一组SQL语句打包成一个整体,在执行这组SQL的过程中,要么全部成功,要么全部失败。也就是说,事务必须满足ACID的特性。

2.介绍分布式事务

所谓的"分布式事务",指的就是在分布式系统中,为了保证数据的一致性和完整性,对多个节点上的数据进行操作的事务。当一个事务涉及到多个不同的数据库,服务,实例的时候就构成了分布式事务。

就像上图,订单服务和商品服务分别独立成了两个数据库,当一个客户端下单时的时候,需要在订单服务对应的数据库中创建订单,同时需要调用库存服务完成商品库存的扣减。订单服务进行操作的时候,订单服务的数据操作和库存服务的数据操作保持一致,此时这个就是"分布式事务"。总的来说,分布式数据一致性问题,就是如何在分布式场景中保证多个节点数据的一致性问题。

(三).分布式事务存在的问题

这是我们将要演示分布式事务存在的问题的测试环境

Account服务,用户服务,管理用户的资金账户,提供扣减余额的接口

Storage服务,库存服务,管理商品禄存,提供扣减库存的接口

Order服务,订单服务,负责管理订单,创建订单的时候,会调用Account服务和Storage服务

sql 复制代码
CREATE DATABASE IF NOT EXISTS seata_test;

use seata_test;

DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`),
  CONSTRAINT `count_chk` CHECK (`count` >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  CONSTRAINT `count_chk_2` CHECK (`count` >= 0),
  CONSTRAINT `money_chk` CHECK (`money` >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  CONSTRAINT `money_chk_2` CHECK (`money` >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 数据
INSERT INTO `storage_tbl` VALUES (1, '2001', 160);
INSERT INTO `storage_tbl` VALUES (2, '2002', 1000);
INSERT INTO `storage_tbl` VALUES (3, '2003', 500);
INSERT INTO `storage_tbl` VALUES (4, '2004', 400);
INSERT INTO `storage_tbl` VALUES (5, '2005', 600);

INSERT INTO `account_tbl` VALUES (1, '1001', 800);
INSERT INTO `account_tbl` VALUES (2, '1002', 2000);
INSERT INTO `account_tbl` VALUES (3, '1003', 1400);
INSERT INTO `account_tbl` VALUES (4, '1004', 2800);
INSERT INTO `account_tbl` VALUES (5, '1005', 3000);

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

上图是我们对应该项目使用的SQL脚本,同时可以看到,针对一些特殊的字段进行了一些特殊的限制,例如,对一些字段进行了check校验,必须大于0

这是测试之前的状态

当进行正常的测试的时候

可以发现,是可以测试成功的

但是当我进行错误测试的时候

在订单服务中,先进行了扣余额的操作,再进行了扣库存的操作

此时,我们将库存增加到了200,那么显然是不符合标准的

在StorageApplication中报错了,

但是对应余额来说,已经扣除完成了

可以看到,订单服务和用户服务都已经操作成功了,唯独库存服务没有执行成功。按照正常的思维,整个事务是需要进行回滚的,但是现在并没有进行回滚。这就是有问题的,这个问题就是分布式事务问题

(四).分布式事务存在问题的理论模型

分布式事务问题也叫做分布式数据一致性问题,即如何在分布式场景中保证多个节点数据的一致性问题。分布式事务问题产生的核心原因在于存储资源的分布性,例如不同的数据库如何保证数据一致性问题。接下来就来介绍分布式事务问题的常见解决方案,在介绍之前,我们需要先了解一些基础理论。

1.CAP理论

在前面介绍Eureka,Nacos等注册中心的时候,就介绍到过CAP理论

CAP理论是分布式系统设计中最基础,也是最关键的理论。

C:一致性,这里指的是"强一致性",所有节点都在同一时间具有相同的数据

A:可用性,保证每个请求都有响应,但是响应的结果可能不对

P:分区容错性,当网络出现分区后,系统仍然能够对外提供服务

在一个分布式系统中,不可能同时满足数据一致性,服务可用性和分区容错性这三个基本需求,最多只能满足其中的两个。这是因为,在分布式系统中,系统网络不可能100%保证健康,服务又必须对外保证服务,所以分区容错性(P)是不可少的,那么就只能在C和A中选一个,即CP架构或者AP架构。

CP架构:为了保证分布式系统对外的数据一致性,所以不返回数据。

AP架构:为了保证分布式系统的可用性,返回当前的数据,即使这个数据不正确。

2.BASE理论

BASE理论是由于CAP中一致性和可用性不可兼得而衍生出来的一种新的思想,BASE理论的思想是通过牺牲数据的强一致性来获得高可用性

Basically Available (基本可用):在分布式系统出现故障的时候,允许损失一部分功能的可用性,保证核心功能的可用

Soft-state (软状态):允许系统中的数据存在中间状态,即允许系统中不同节点的数据副本之间的同步存在演示,这个状态不影响系统的可用性。

Eventually Consistent (最终一致性):中间状态的数据在经过一段时间之后,会达到一个最终的数据一致性。

例如,在某宝双十一的时候,此时处于流量的高峰期,那么某宝就可以放弃一些广告推广的功能,来保证双十一的正常可用。这就是 Basically Available。

例如,用户A,用户B和用户C,三个用户互为好友,当A发了一个朋友圈,B看到了,C没有看到,但是过段时间之后,C才看到。对于"B看到了,C没有看到"这个操作就是 Soft-state ;对于"过段时间之后,C才看到"这个操作就是 Ecentually Consistent。

BASE理论不要求数据的一致性,它允许数据在一段时间内是不一致的,但是数据最终会在某个时间点实现一致

BASE理论和CAP理论的对比

CAP理论:一个分布式系统不可能同时满足一致性,可用性,分区容错性三个特性。

BASE理论:BASE理论是对CAP理论的补充,通过放宽对一致性的要求,换取系统更高的可用性和灵活性。如果不是必须的话,不推荐使用事务或强一致性,允许在牺牲一定一致性的前提下获得更高的可用性。

3.X/Open分布式事务模型

X/Open是一个组织,X/Open DTP 是X/Open这个组织定义的一套分布式事务的标准 ,这个标准提出了两阶段提交(2PC,Two-Phase-Commit)来保证分布式事务的完整性

X/Open包含了三种角色:

AP:Application,应用程序

RM:Resource Manager,资源管理器,例如数据库。应用程序可以通过资源管理器对相应的资源进行有效的控制。

TM:Transaction Manager,事务管理器,事务协调者,负责协调和管理各个子事务,即管理RM

TM出现的场景:·在分布式系统中,会有多个节点,每一个节点都知道自己的事务结果是成功或者失败的,但是获取不到其他节点的事务操作结果 。所以当一个事务跨越了多个分布式节点的时候,为了保证事务的ACID特性,就需要一个"协调者(TM)"来统一调度所有分布式节点的执行逻辑 ,这些被调度的节点被称为"参与者"(RM)",TM负责调度RM的行为,并最终决定这些参与者是否要把事务真正提交

X/Open DTP模型的执行步骤:

①.配置TM,把多个RM注册到TM

②.AP从TM管理的RM中获取连接,例如JDBC

③.AP向TM发起一个全局事务,申城全局事务XID,XID会通知各个RM

④.AP通过第二获得的连接直接操作RM完成数据操作。AP在每次操作的时候都会把XID传递给RM

⑤.AP结束全局事务,TM会通知各个RM全局事务结束。根据各个RM的事务执行结果,执行提交或者回滚。

4.两阶段提交

(1).概念

对于两阶段提交,X/Open DTP标准就是使用了两阶段提交来保证分布式事务的完整性,TM对多个RM事务的管理,就会涉及到两个阶段的提交。第一阶段是事务的准备阶段,第二阶段是事务的提交或者回滚阶段。

(2).执行步骤

准备阶段:

①.协调者发送请求:协调者向所有的参与者发送prepare请求 ,询问他们是否准备提交事务。在请求中,包含了事务的详细信息,要求参与者对事务进行预处理,并准备好回滚或者提交事务所需要的所有资源

②.参与者响应请求:参与者收到prepare请求后,会执行事务操作(锁定资源),但不提交。如果参与者执行事务成功了,此时会将事务的执行结果和准备状态记录在本地日志中,并向协调者发送ready消息,表示已经准备好了提交事务;如果执行失败或者无法准备,则向协调者发送abort消息。

提交阶段:

①.协调者根据准备阶段的反馈进行决策:当协调者收到所有参与者的相应之后,会根据反馈的结果做出决策,如果所有参与者都返回ready,则协调者提交事务;如果有任何一个参与者返回abort,则协调者决定回滚事务。注意:这里指的是回滚整个全局事务,全局事务包含了多个子事务。

②.协调者发送提交或回滚事务:A.提交事务:协调者向所有的参与者发送commit请求。参与者接收到请求之后,会正式提交事务,并释放所有资源,然后向协调者发送ack消息,表示事务已成功提交。

B.回滚事务:协调者向所有的参与者发送rollback请求。参与者接收到请求之后,会回滚事务,并释放所有的资源,然后向协调者发送ack消息,表示事务已成功回滚。

(3).优缺点

优点:

①.保证了事务的原子性,通过两个阶段的严格控制,确保了事务要么全部提交,要么全部回滚,从而保证了事务的原子性

②.实现相对简单:相对于其他分布式事务协议,实现相对简单,易于理解和实现。

缺点:

①.阻塞问题:两个阶段都是事务阻塞型的,对于每一个指令都有明确的响应,如果在这个过程中,TM宕机或者网络出现故障,则会一直处于阻塞状态。"三阶段提交"解决了这个问题

②.资源占用:参与者收到preprare请求后,会执行事务,锁定相关资源来保证事务的原子性。在整个两个阶段提交过程中,这些资源一直被锁定,直到提交或者回滚完成。导致资源利用率第,其他事务可能因为无法获取所需资源而等待。

③.数据不一致:第二阶段中,TM向所有的RM发送commit请求,由于网络异常,可能会导致一部分RM没有收到commit请求,所以事务无法提交,出现数据不一致的情况

5.三阶段提交

(1).概念

3PC(Three-Phase-Commit),是2PC的改进版本,分为CanCommit,PreCommit和DoCommit三个阶段。

(2).执行步骤

CanCommit阶段:

①.协调者发送请求:协调者向所有的参与者发送CanCommit请求,询问他们是否可以执行事务提交操作,这个阶段不涉及到实际的数据修改,只用来确认每个参与者是否有足够的资源和条件来完成事务。

②.参与者响应:参与者根据自身情况返回Yes或No,如果所有的参与者都返回Yes,则进入PreCommit阶段;只要有一个返回No,则全局事务发生回滚。

PreCommit阶段:

①.协调者发送PreCommit请求:协调者向所有的参与者发送PreCommit请求,询问是否可以进行事务的预处理操作。

②.参与者准备事务:参与者执行事务操作,并将事务的执行结果和准备状态(Yes/No)发送给协调者。参与者会记录预提交日志,并确保这些日志是持久化的。

③.协调者收集反馈并决策:如果所有参与者都返回Yes,则进入DoCommit阶段,如果有任何一个参与者返回No或超时未响应,协调者会发送abort请求,通知所有参与者回滚事务。

DoCommit阶段:

①.协调者发送DoCommit请求:协调者向所有参与者发送DoCommit请求,指示他们正式提交事务。

②.参与者执行提交:参与者收到DoCommit请求后,执行事务提交操作,并向协调者发送Ack消息,表示事务已提交。

③.超时机制:如果参与者在等待DoCommit请求时超时,会默认执行提交操作

(3).优缺点

优点:

①.减少阻塞:三阶段提交通过引入了超时机制,减少了二阶段提交的阻塞问题,避免了资源被永久锁定。

②.增强容错能力:及时协调者在DoCommit阶段之前出现故障,参与者也可以基于其预提交的状态自主决定继续提交或回滚事务,从而减少了对协调者的依赖。

缺点:

①.实现复杂度高:三阶段提交的实现比二阶段提交更复杂,增强了系统的开发和维护成本

②.数据不一致风险:在某些情况下,例如网络分区。参与者收到PreCommit消息后,如果网络出现故障,协调者和参与者无法进行后续通信,参与者在超时后可能会自动提交事务,导致数据不一致。

6.TCC事务

(1).概念

TCC(Try-Confirm-Cancel)是一种分布式事务方案。TCC事务相对于传统的两阶段提交,其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对业务逻辑(有业务系统提供)的接口调用来实现分布式系统。

(2).执行步骤

TCC通过将事务操作拆分为3个阶段:Try阶段,Confirm阶段,Cancel阶段

Try阶段:

尝试执行业务操作,完成所有业务的检查,并预留必要的业务资源,这个阶段不真正执行事务,只是进行资源的预占。

Confirm阶段:

如果所有参与者在Try阶段都成功,那么进入Confirm阶段,正式完成操作,使用之前预留的资源。

Cancel阶段:如果任何一个参与者在Try阶段失败,那么进入Cancel阶段,所有参与者回滚在Try阶段的操作,释放预留的资源。

例如"库存扣减"操作,假设库存数量为100个,本次购买20个

Try阶段:对库存数量进行冻结

Confirm阶段:把Try阶段冻结的库存数量进行实际的扣减

Cancel阶段:把Try阶段冻结的库存数量进行解冻

(3).优缺点

优点:

①.无需依赖第三方中间件或数据库来实现分布式事务,降低了系统复杂度和成本

②.无需锁定全局资源,提高了系统的并发性能和利用性

③.适用于各种类型的业务场景,只要能够定义出清晰的Try,Confirm和Cancel逻辑

缺点:

①.需要手动编写三个阶段的业务逻辑,并保证其正确性和一致性,增加了开发难度和维护成本

②.需要考虑各种异常情况和边界情况,并提供响应的补偿策略和重试机制,增加了系统的复杂度和风险。

(五).初识Seata

1.Seata术语

TC(Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚

TM(Transaction Manager):事务管理器,定义全局事务的范围:开启全局事务,提交或回滚全局事务

RM(Resource Manager):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

Seata分为TC TM RM三个角色,TC 为单独服务端部署,TM和 RM 由业务系统集成

2.微服务集成Seata

这里我已经将Seata下载好了,并修改了配置文件,我使用的是Nacos

(1).引入依赖

在需要的微服务中引入Seata依赖

XML 复制代码
        <!--   添加seata依赖     -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

(2).修改配置文件

在application.yml中添加Seata相关配置。根据配置信息,确定的那个TC服务地址

XML 复制代码
#Seata的相关配置,通过这些配置,使得account-service客户端能够找到Nacos上的seata-service配置,前提是seata-service要注册到注册中心Nacos上
seata:
  registry: #定义了Seata Server的注册中心配置,微服务根据配置信息去注册中心获取tc服务地址
    type: nacos  #指定注册中心的类型
    nacos:
      application: seata-server  #Seata Server在Nacos中的应用名称
      server-addr: 127.0.0.1:8848  #Nacos服务器地址
      group: "SEATA_GROUP"  #Seata Server在Nacos中的分组名称
      namespace: ""  #Nacos的命名空间,设置为空,表示使用默认的命名空间public
  tx-service-group: default_tx_group  #定义事务服务组的名称
  service:
    vgroup-mapping:
      default_tx_group: default

配置文件中的配置要和Nacos中的服务列表对应起来的

下面主要来介绍这部分配置信息,这些配置信息就使事务分组找到了后端Seata集群

seata.tx-service-group:定义了事务服务组的名称,这里设置为default_tx_group。事务服务组用于将Seata Server 和Seata Client进行分组管理,确保他们能够正确的发现和通信。

seata.service.vgroup-mapping.事务分组名:定义了Seata Server的服务配置,事务服务组到Seata Server集群的映射关系,这里将defalut_tx_group 映射到default集群,这里的default指的是Seata Server的集群名称

上面的配置信息。在微服务启动时,会根据default_tx_group查找Seata Server。Seata Server在启动时会注册到Nacos的SEATA_GROUP分组中,应用名称为 seata-server,并映射到default集群,这样,Seata Client可以通过Nacos发现并连接到Seata Server,进行事务协调和管理。

这里,指定的集群是default集群中的机器,因为,在Nacos上,我们可以设置多个集群,所以我们需要指定一下特定的集群

(六).Seata的各种事务模式

Seata提供了多种事务模式,为开发者提供了一站式的分布式事务解决方案。

1.XA模式

(1).模式介绍

XA模式是利用事务资源(数据库等)对XA协议的支持,以XA协议的机制来管理分支事务的一种十五模式。XA实现的原理是基于两阶段提交实现的。

执行步骤:

①.开启事务:TM开启一个全局事务,并与TC建立连接,TC返回一个全局事务ID(XID)给TM

②.分支事务注册与执行:RM收到业务操作请求后,会向TC注册分支事务,执行业务SQL,并携带XID来保证事务的一致性

③.分支事务状态报告:RM执行完分支事务后,向TC报告分支事务的执行状态

④.事务提交或回滚决策:TM在所有分支事务执行完毕后,会通知TC事务结束。TC接收到事务结束通知后,会检查各分支事务的执行状态,如果所有分支事务都执行成功,则TC通知所有RM提交事务;如果有任意一个分支事务执行失败,则TC通知所有RM回滚事务

⑤.分支事务提交或回滚:RM接收到TC的提交或回滚指令后,执行响应的commit或rollback操作

(2).配置和使用

Ⅰ.在application.yml中配置Seata的事务模式
Ⅱ.给发起全局事务的入口方法添加 @GlobalTransactional注解
Ⅲ.进行测试

当进行正常的测试的时候

这是测试之前的数据库截图

这是测试之后的数据库截图

下面看后端日志

当进行异常的测试的时候

这是测试之前的数据库截图

这是测试之后的数据库截图

下面看后端日志

(3).优缺点

优点:

①.事务强一致性:XA模式能够满足ACID原则,确保分布式事务的强制一性

②.实现简单并且无代码侵入:常用数据库都支持XA协议,使用Seata的XA模式无需修改业务代码,只需要进行简单的配置即可

缺点:

①.性能较差:一阶段需要锁定数据库资源,等待二阶段结束才释放,导致事务资源长时间得不到释放,锁定周期长,从而影响性能

②.依赖关系数据库:XA模式依赖数据库实现事务,对于一些非关系型数据库或者不支持XA协议的数据库,无法使用

2.AT模式

(1).模式介绍

AT模式是Seata创新的一种非侵入方式的分布式事务解决方案。Seata在内部做了对数据库操作的代理层。在使用Seata 的AT模式时,实际上使用的是Seata自带的数据源代理DataSourceProxy,Seata在这层代理中加了很多逻辑,例如插入回滚undo_log日志,检查全局锁等等。

整体机制:

第一阶段:

①.注册分支事务:TM注册全局事务,RM向TC注册分支事务。

②.记录undo_log:RM在执行SQL操作之前,会先解析SQL语句,记录SQL更新前的快照和更新后的快照到undo_log日志表中。在undo_log表中,记录了足够的信息,来便于需要回滚时能够回复的数据。

③.执行SQL并提交本地事务:RM执行业务SQL操作,并直接提交本地事务,此时数据会真实的提交到数据库中

④.报告事务状态:RM向TC报告分支事务的执行状态,告知其本地事务已经提交。

第二阶段:

①.提交成功:如果所有的分支事务都提交成功,TC会通知RM清理undo_log相关的补偿信息,完成整个分布式事务的处理

②.提交失败:如果由任意一个分支事务失败,TC会通知RM进行回滚。RM根据undo_llog中的补偿信息对数据进行反向补偿,实现事务的回滚。

(2).读写隔离

Ⅰ.写隔离

当多个线程并发操作同一个数据的时候,可能会出现脏写问题

按道理来说,应该回滚到800,但是回滚到了1000

Seata的AT模式通过引入全局锁 来解决这个问题。在释放本地锁之前,先拿到全局锁,避免同一时刻有另一个事务来操作当前数据。如果拿不到全局锁,则不能提交本地事务

tx1先开始,开启本地事务,拿到了本地锁,然后执行了更新操作。在本地事务提交之前,先拿到了该记录的全局锁,然后本地提交释放本地锁。tx2后开始,tx2开启本地事务,拿到本地锁,然后执行了更新操作。tx2本地事务提交前,先要拿到该记录的全局锁,tx1全局事务提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁只有当tx1二阶段全局提交,释放全局锁之后,tx2才能拿到全局锁,提交本地事务

如果tx1的二阶段发生了回滚操作,那么tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。但是,tx2仍然在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败,分支的回滚会一直重试,一直等到tx2的全局锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。在整个过程中,全局锁在tx1结束前一直被tx1持有,不会发生脏写问题。

Ⅱ.读隔离

在数据库本地事务隔离级别"读已提交 "或以上的基础上,Seata的AT模式的默认全局事务隔离级别是"读未提交"。如果应用在特定场景下,必须要求全局的"读已提交",目前Seata的方式是通过SELECT FOR UPDATE语句的代理。

SELECT FOR UPDATE语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁并重试,在这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。

(3).工作机制

第一阶段

①.解析SQL,AT模式会得到SQL的类型,操作的表以及条件语句等相关信息。

②.查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

③.执行业务SQL:更新数据

④.查询后镜像:根据前镜像的结果,通过主键定位数据。

⑤.插入回滚日志:把前镜像和后镜像的数据以及业务SQL相关的信息组成一条回滚日志记录,插入到 undo_log表中

⑥.申请插入数据的表中,主键值等于该数据的全局锁

⑦.本地事务提交:业务数据的更新和前面步骤生成的undo_log表中的数据一并提交

⑧.将本地事务提交的结果上报给TC

第二阶段-回滚

①.收到TC的分支回滚请求,开启一个本地事务,执行下面的操作

②.通过XID(全局事务ID)和Branch ID(分支事务ID)查找到相应的undo_log记录

③.数据校验:拿到undo_log中的后镜与当前数据进行比较,如果不相同,则说明数据被当前全局事务之外的动作做了修改,此时需要根据配置策略来做处理。

④.根据undo_log中的前镜像和业务SQL的相关信息生成并执行回滚的语句

⑤.提交本地事务,并把本地事务的执行结果上报给TC

第二阶段-提交:

①.收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC

②.异步任务阶段的分支提交请求将异步和批量地删除相应的undo_log记录

(4).配置使用

Ⅰ.创建对应的数据库表
sql 复制代码
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Ⅱ.配置事务模式为AT
Ⅲ.进行测试

上面是测试之前的数据库表记录

这是执行完成之后截图

下面看后端日志

下面测试异常情况

这是执行之前的截图

这是执行之后的截图

下面看后端日志

(5).AT和XA的区别

①.从实现方式上看,AT模式是通过记录数据快照来实现数据的回滚,适用于几乎所有的数据库,但是需要在业务库中创建undo_log表;XA模式依赖数据库对XA协议的支持,通过XA规范来实现事务的提交和回滚,XA模式不需要额外的undo_log表,但要求数据库支持XA协议

②.从一致性上看,AT模式是最终一致性,在事务的两阶段之间,数据可能处于中间状态;XA模式是强一致性,事务的中间状态对用户不可见

③.从性能上看,AT模式性能较好,一阶段直接提交,不锁定资源;XA模式性能较差,一阶段锁定资源,等待二阶段结束才释放

④.从代码侵入上看,AT模式和XA模式都没有代码侵入

⑤.从数据库支持上看,AT模式适用于几乎所有支持SQL的数据库;XA模式以来数据库对XA协议的支持

如果对业务对性能要求很高并且可以接受最终一致性,则使用AT模式

如果对业务对数据一致性要求极高且对性能要求不高,则使用XA模式

3.TCC模式

(1).模式介绍

TCC模式是Seata支持的一种由业务方细粒度控制的侵入式分布式事务的解决方案。其分布式事务模型直接作用于服务层,不依赖于底层数据库,可以灵活选择业务资源的额锁定粒度,减少资源锁持有时间,可扩展性好。

Seata的全局事务,整体是两阶段提交的模型。全局事务是由若干个分支事务组成,每个分支事务都需要具备自己的一阶段prepare行为和二阶段commit或rollback行为

TCC模式,不依赖于底层数据资源的事务支持:

一阶段prepare行为:调用自定义的prepare逻辑

二阶段commit行为:调用自定义的commit逻辑

三阶段rollback行为:调用自定义的rollback逻辑

用户实现TCC服务之后,该TCC服务将作为分布式事务的其中一个资源,参与到整个分布式事务中。事务管理器二阶段协调TCC服务,在第一阶段调用所有的TCC服务的Try方法,在第二阶段执行所有TCC服务的Confirm或者Cancel方法,最终所有的TCC服务要么全部提交要么全部回滚

(2).TCC设计

Ⅰ.业务操作分析

这里,以库存扣减为例

Try操作:进行资源查看和预留。先检查是否够,这个姐u但不发生真正的扣库存

Confirm操作:执行真正业务的提交

Cancel操作:预留资源的是否释放,让商品回到初始状态

Ⅱ.并发控制

在一阶段Try操作,分布式事务T1和分布式事务T2分别冻结一部分资金,两者相互不干扰。这样,在第二阶段,无论T1是提交还是回滚,都不会对T2产生影响。

Ⅲ.允许空回滚

事务协调器在调用TCC服务的一阶段Try操作的时候,可能会出现因为丢包而导致的网络超时,此时事务管理器会触发二阶段回滚,调用TCC服务的Cancel操作,Cancel操作调用未发生超时,TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景称为"空回滚"

Ⅳ.防悬挂控制

事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为网络拥堵而导致的超时,此时事务管理器会触发二阶段回滚,调用TCC服务的Cancel操作。在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段的Cancel请求比一阶段Try请求先执行的情况,此时TCC服务在执行晚到的Try之后,将永远不会再收到二阶段的Confirm或者Cancel,造成TCC服务悬挂

Ⅴ.幂等控制

无论是网络包重传,还是异常事务的补偿执行,都会导致TCC服务的Try,Confirm或者Cancel操作被重复执行。

Ⅵ.Seata的解决方式

在Seata1.5.1版本中,增加了一张事务控制表 "tcc_fence_log",包含事务的XID和RranchID信息,来解决这个问题。

sql 复制代码
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;

空回滚:

在Try方法执行时插入一条记录,表示一阶段执行了,执行Cancel方法时读取这条记录,如果记录不存在,说明Try方法没有执行,以此来避免空回滚。

悬挂:

在Rollback阶段,如果查询事务控制表中没有记录,说明Cancel咸鱼Try执行了,因此插入一条status=4的状态记录。当执行Try阶段的时候,判断status=4,则说明有二阶段Cancel已经执行,并返回false来阻止一阶段的Try方法执行成功

幂等:

在TCC事务控制表中增加一个记录状态的字段status,该字段有4个值:

tired(1):表示Try阶段已经执行过

committed(2):表示二阶段Commit已经执行完成

rollbacked(3):表示二阶段Rollback已经执行完成

suspended(4):表示空回滚/悬挂/中止状态

二阶段Confirm/Cancel方法执行后,将状态修改为commited或者rollbacked状态,当重复调用二阶段Confirm/Cancel方法时,判断事务状态即可解决幂等问题

(3).TCC实现

以库存服务(storage-service)为例

Ⅰ.创建事务控制表
sql 复制代码
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;
Ⅱ.添加冻结字段

为了完成一阶段Try资源的预检,以及T2阶段事务的提交或回滚,需要增加一个新的字段,表示冻结部分

sql 复制代码
-- 添加冻结字段
ALTER TABLE storage_tbl ADD COLUMN freeze_count INT(11) unsigned DEFAULT 0
COMMENT '冻结库存';

同时,修改对应的实体类

Ⅲ.TCC接口定义与实现

在库存服务中定义TCC接口

实现TCC接口

java 复制代码
package com.bite.storage.service.impl;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.bite.storage.entity.StorageInfo;
import com.bite.storage.mapper.StorageMapper;
import com.bite.storage.service.StorageTccService;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
@Slf4j
@LocalTCC

public class StorageTccServiceImpl implements StorageTccService {

    @Autowired
    private StorageMapper storageMapper;

    @TwoPhaseBusinessAction(name = "storageDeduct",commitMethod = "commit",
            rollbackMethod = "rollback",useTCCFence = true)  //通过useTCCFence属性就可以使TCC自动帮我们处理空回滚,悬挂,幂等等关系
    @Override
    public boolean prepare(@BusinessActionContextParameter("commodityCode") String commodityCode,
                           @BusinessActionContextParameter("count") Integer count) {
        log.info("一阶段try.....");
        try {
            UpdateWrapper<StorageInfo> updateWrapper=new UpdateWrapper<>();
            updateWrapper.setSql("count=count-"+count);
            updateWrapper.setSql("freeze_count=freeze_count+"+count)    //让冻结字段加上相应的值
                    .lambda().eq(StorageInfo::getCommodityCode,commodityCode);
            storageMapper.update(updateWrapper);
            return true;
        }catch (Exception e){
            log.error("扣减库存失败, e:", e);
            throw new RuntimeException("扣减库存失败!", e);
        }

    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        log.info("二阶段commit.....");
        //进行库存的扣减(冻结库存)
        String commodityCode = (String) actionContext.getActionContext("commodityCode");
        Integer count = (Integer) actionContext.getActionContext("count");
        
        UpdateWrapper<StorageInfo> updateWrapper=new UpdateWrapper<>();
        updateWrapper.setSql("freeze_count=freeze_count-"+count)    //让冻结字段减去相应的值
                .lambda().eq(StorageInfo::getCommodityCode,commodityCode);
        
        int result=storageMapper.update(updateWrapper);
        return result==1;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        log.info("二阶段rollback.....");

        //进行回滚
        String commodityCode = (String) actionContext.getActionContext("commodityCode");
        Integer count = (Integer) actionContext.getActionContext("count");

        UpdateWrapper<StorageInfo> updateWrapper=new UpdateWrapper<>();
        updateWrapper.setSql("count=count-"+count)              //让原来的库存加上冻结的值
                .setSql("freeze_count=freeze_count-"+count)    //让冻结字段减去相应的值
                .lambda().eq(StorageInfo::getCommodityCode,commodityCode);

        int result=storageMapper.update(updateWrapper);
        return result==1;
    }
}

然后在Controller层中修改指向

下面进行测试

status=2的状态

status=2表示正常提交

可以看到,status=2

status=1的状态

status=1表示Try阶段已经执行过了

我们可以直接在第二阶段这里,打上断点,然后开始调试

现在已经在调试了

此时,可以发现,status就为1

status=4的状态

status=4是发生了空回滚/悬挂/中止状态

可以看到,status=4

status=3的状态

status=3是进行了回滚

我在余额项目中手动抛异常

当余额抛出异常的时候,库存项目已经执行完成了,但是由于余额项目抛出异常,所以库存项目也不得不进行事务回滚操作

Ⅳ.优缺点:

优点:

①.性能高:TCC模式没有全局锁,事务的提交和回滚都是由应用代码实现,资源锁定时间短,性能较高;

②.灵活性强:可以自定义Try Confirm Cancel逻辑,适合复杂的业务场景

③.不依赖数据库事务:可以提供过更细粒度的控制,可以用非事务型数据库

缺点:

①.开发成本高,需要自己编写Try Confirm Cancel接口

②.适用场景有限:对于复杂的业务,补偿逻辑可能会非常复杂,补偿成本过高的业务不适合使用TCC模式

4.Saga模式

(1).介绍

Saga模式是Seata提供的长事务解决方案

Saga由一系列sub-transaction Ti组成,每个Ti都有对应的补偿动作Ci,补偿动作用于撤销T造成的数据变更结果,它和TCC相比,少了Try这个预留动作,每个T操作都真实的影响数据库。

(2).适用场景

①.业务流程长,业务流程多

②.参与者包含其他公司或遗留系统服务,无法提供TCC模式要求的三个接口

(3).优缺点

优点:

①.一阶段提交本地事务,无锁,高性能

②.事务驱动架构,参与者可异步执行,高吞吐

③.补偿服务易于实现

缺点:

不保证隔离性

5.四种模式对比

|-------|------------------------------------------------------------------|--------------------------------------------------------|------------------------------|-----------------------------------------|
| | XA | AT | TCC | SEGA |
| 实现方式 | ①.依赖数据库对XA协议的支持,通过XA规范来实现事务的提交和回滚 ②.不需要额外的undo_log表,但要求数据库支持XA协议 | ①.通过记录数据快照来实现数据的回滚 ②.适用于几乎所有的数据库,但是需要在业务库中创建undio_log表 | 开发人员手动实现Try,Confirm,Cancel阶段 | ①.事件驱动,每个事务包含正向操作和逆向补偿操作 ②.失败时按顺序执行逆向补偿 |
| 一致性 | 强一致性,事务的中间状态对用户不可见 | 最终一致性 在事务的两阶段之间,数据可能处于中间状态 | 最终一致性(通过业务代码实现) | 最终一致性 |
| 性能 | 较差,一阶段锁定资源,等待二阶段结束后才释放资源 | 较好,一阶段直接提交,不锁定资源 | 较高,开发成本高,需要处理空回滚等 | 高,无锁,适合长事务 |
| 代码侵入 | 无(微小) | 无 | 有,需要手动编写三个接口 | 有,需要编写状态机和补偿业务 |
| 数据库支持 | 依赖数据库对XA协议的支持 | 适用于几乎所有支持MYSQL的数据库 | 不依赖底层数据库的事务机制 | 不依赖底层数据库的事务机制 |

相关推荐
写后端的胖头鱼1 小时前
【高频面试题】分布式锁在项目中的应用
java·分布式·后端·分布式锁·高频面试题
凤山老林1 小时前
Spring Boot + OpenSearch 实战:搞定全文检索、向量召回与混合排序
spring boot·后端·全文检索·向量·opensearch·全文索引
梅梅绵绵冰1 小时前
SpringCloud网关
java·spring·spring cloud
IT_陈寒2 小时前
Vite的HMR怎么突然罢工了?原来是我漏了这个配置
前端·人工智能·后端
BingoGo2 小时前
一个ChatGPT 超级省额度方案!用 TaskQuay 连接网页 ChatGPT 和本地 Codex
人工智能·后端
QQ_21696290962 小时前
基于微服务架构的店铺管理系统的设计与实现
大数据·spring boot·后端·spring·微服务·小程序·架构
芒鸽2 小时前
把 Agent 运行时做成插件系统:agent-harness(openJiuwen Rust版) 的实践
开发语言·后端·rust
CHHH_HHH2 小时前
【Linux系统篇】进程间通信揭秘:从管道到共享内存
linux·服务器·c语言·开发语言·后端·ubuntu
mldong2 小时前
Node 开发者也有自己的轻量工作流引擎了:npm i 一行,5 分钟跑通一条审批流
javascript·后端·typescript