淘先锋技术网

首页 1 2 3 4 5 6 7



本篇内容为学习Seata 的课后总结,本文大致包括以下内容:

  • 分布式事务的介绍;
  • CAP定理与BASE理论;
  • Seata介绍;
  • Seata的四种事务模式;

整个过程记录详细,每个步骤亲历亲为实测可用


本篇文章是笔者初步学习Seata后的课后总结。编写文章的时候,脑子有点不在线。qaq

一、分布式事务

在我们之前的单体架构中,通常也存在完成一个业务需要调用多个方法。例如:用户完成订单操作,需要去增加一条订单数据、修改库存数量、修改用户账户余额等。当整个调用链中的某个方法出现故障时,其他方法正常执行时,就会存在例如数据不一致等问题。

我们之前则是采用 事务 的方法来解决这些问题的。健全的事务处理也是关系型数据库MySql 的一大特点

聊到事务,我们就必须明确事务的四大原则 ACID

  • A(Atomicity):原子性。是指事务中的一系列操作,要么同成功,要么同失败。
  • C(Consistency):一致性。保证数据库内部的完整性约束、声明性约束。
  • I(Isolation):隔离性。对同一资源的事务不能同时进行。(利用加锁机制来实现)
  • D(Durability):持久性。对数据库的修改将永久保存,无论是否发生意外。

之前我们聊到的是单体架构的系统中的事务问题。那么什么是分布式事务呢?

:在分布式系统下,一个业务跨越多个服务或数据源,每一个服务都是一个分支事务 ,要保证所有分支事务最终状态一致,这样的事务就称为 分布式事务

在这里插入图片描述


在上一小节,我们简单回顾了事务的四大原则,以及了解了分布式事务的概念。下一小节,我们将学习分布式事务中的重要定理、理论、概念等。

二、CAP定理与Base理论

1. CAP定理

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:

  • C(Consistency):一致性;
  • A(Availability):可用性。
  • P(Partition Tolerance):分区容忍性。

Eric Brewer 认为,分布式系统无法同时满足这三个指标(只能均衡选择这其中的两个指标),这个结论就称为 CAP定理
在这里插入图片描述

下面我们将对这三个指标进行详细讲解:

a) Consistency (一致性):一致性是指用户在访问分布式系统的任意节点,所得到的结果必须保持一致。
在这里插入图片描述

b) Availability(可用性):用户访问分布式系统中的任意健康节点,必须得到响应,而不是超时或拒绝。
在这里插入图片描述
c) Partition (分区):因为网络故障或其他原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容忍性) :在集群出现分区时,整个系统也要持续对外提供服务。
在这里插入图片描述

下面做个简单的总结

简述CAP定理的内容:

  • 分布式系统节点通过网络连接连接,一定会出现分区问题(P)
  • 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足。

拓展: ElasticSearch 集群是CP 还是 AP?
:在ES集群中,故障节点会被剔除集群,故障节点之前存留的数据分片会重新分配到其它节点,以保证数据的一致性。因此是低可用性,高一致性,属于CP。

2. BASE理论

BASE理论是对CAP 的一种解决思路(主要为了解决分布式系统只能绝对满足CAP其中的两条),包含三个思想:

  • Basically Available(基本可用):分布式系统出现故障时,允许损失部分可用性,即保证核心可用
  • Soft State(软状态): 允许出现中间状态,例如临时的数据不一致性。
  • Eventually Consistent(最终一致性): 虽然无法保证强一致性,但是在软状态结束后,可以达到数据最终一致性

分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理BASE理论 提出解决分布式事务的两种思想 :

  • AP模式(最终一致性思想) :各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据,实现数据的最终一致性。
  • CP模式(强一致性思想):各子事务执行后相互等待,同时提交,同时回滚,达成强一致。但在事务等待过程中,整个系统处于弱可用状态。

3. 分布式事务模型

解决分布式事务,各个子系统之间必须能感知彼此的事务状态,才能保证一致性,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务)。

子系统事务又称为分支事务,有关联的各个分支事务组合在一起称为全局事务
在这里插入图片描述


在上一小节,我们了解了解决分布式事务问题的重要理论和定理。在下一小节,我们将正式进入Seata 来解决分布式事务问题。

三、Seata 介绍

Seata 是蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

1. Seata相关概念

Seata 事务管理中有三个重要角色:

  • TC(Transitional Coordinator) 事务协调者:维护全局和分支事务的状态,协调全局事务的提交或回滚。
  • TM(Transitional Manager) 事务管理器:定义全局事务的范围、全局事务的起点,负责开启全局事务、提交或回滚全局事务。
  • RM(Resource Manager)资源管理器 :管理分支事务处理的资源,分支事务与TC进行交流的中间媒介。负责向TC注册分支事务和报告分支事务状态,并驱动分支事务的提交或回滚。

在这里插入图片描述

Seata基于上述架构提供了四种不同的分布式事务解决方案

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入;
  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式;
    • TCC模式:最终一致的分阶段事务模式,有业务侵入;
  • AGA模式:长事务模式,有业务侵入;

无论哪种方案,都离不开TC,也就是事务的协调者。


2. Seata的部署

a) 下载seata-server 的压缩包,并解压在一个非英文的目录下。
在这里插入图片描述

b) 修改相关的配置(修改conf 目录下的registry.conf 文件)

该文件主要是配置seata 的注册中心与配置中心。

	registry {
	  # 注册中心类型 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
	  type = "nacos"
	
	  nacos {
	    application = "seata-tc-server"
	    serverAddr = "127.0.0.1:8848"
	    group = "DEFAULT_GROUP"
	    namespace = ""
	    cluster = "CQ"
	    username = "nacos"
	    password = "nacos"
	  }
	}
	
	config {
	  # 配置中心类型 file、nacos 、apollo、zk、consul、etcd3
	  type = "nacos"
	
	  nacos {
	    serverAddr = "127.0.0.1:8848"
	    namespace = ""
	    group = "SEATA_GROUP"
	    username = "nacos"
	    password = "nacos"
	    dataId = "seataServer.properties"
	  }
	}

修改成这样即可,注意与后面微服务集成Seata 中的配置相对应。

c) 在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

d) 创建数据库表
tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,需要新建一个名为seata的数据库,提前创建好这些表。

	-- ----------------------------
	-- 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
	-- ----------------------------

e) 启动seata-server

在bin目录下,打开命令行窗口,执行:
在这里插入图片描述

执行成功后,我们即可通过nacos 控制台的服务列表查看相关信息:
在这里插入图片描述

3. 微服务集成Seata

整个业务的分布式事务所涉及的服务都需要进行此操作!

a)导入相关依赖。

	<!-- seata相关依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>seata-spring-boot-starter</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.4.2</version>
    </dependency>

b) 添加相关配置。 让微服务通过注册中心(nacos)找到seata-tc-server:

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中的服务名称
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: CQ

微服务如何根据这些配置寻找TC的地址呢?

我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

  • namespace:命名空间
  • group:分组
  • application:服务名
  • cluster:集群名

以上四个信息,在刚才的yaml文件中都能找到:
在这里插入图片描述


在上一小节,我们完成了Seata的集成和安装。在下一小节,我们将学习Seata中的四种不同的事务模式。

四、Seata 的四种事务模式

1. XA模式

a) 理论

XA规范是分布式事务处理的标准,规定了全局TM与局部RM之间的接口,几乎主流的数据库都对XA规范提供了支持。

在这里插入图片描述

seata 的XA模式做了一些调整,但大体相似:
在这里插入图片描述整个步骤如下所示:

  1. 由TM首先向TC注册全局事务
  2. TM调用分支
  3. 分支事务借助RM 向TC注册分支事务
  4. 每个分支执行业务分支
  5. 分支事务借助RM 向TC报告事务状态
  6. TM 向TC发起全局事务的询问
  7. TC 根据全局事务 来检查其下的分支事务的执行情况。如果全部执行正常,全局事务则直接提交;如果存在分支业务执行失败,则让该全局事务下的所有分支事务进行回滚操作。

XA模式的特点是,每个分支事务执行各自的业务sql 完毕后,不直接提交业务,而是等待其他分支事务的执行。全部执行完毕了,再进行决定是否提交 还是回滚。

XA模式的优点:

  • 事务的强一致性,满足ACID原则。
  • 常用数据库都支持,实现简单,没有代码侵入。

XA模式的缺点:

  • 分支事务需要锁定数据库资源,等待所有分支事务执行完毕后,才会释放,性能较差。
  • 依赖关系型数据库实现事务。

b) 实现
Seata 的 starter 已经完成了XA 模式的自动装配,实现非常简单,步骤如下:
① 修改 application.yml 文件(每个参与事务的微服务都需要进行配置),开启XA模式:

seata:
  data-source-proxy-mode: XA # 开启数据源代理的XA模式

② 给发起事务的入口方法添加 @GlobalTransactional 注解:

    @Override
//    @Transactional
    @GlobalTransactional
    public Long create(Order order) {
        // 创建订单
        orderMapper.insert(order);
        try {
            // 扣用户余额
            accountClient.deduct(order.getUserId(), order.getMoney());
            // 扣库存
            storageClient.deduct(order.getCommodityCode(), order.getCount());

        } catch (FeignException e) {
            log.error("下单失败,原因:{}", e.contentUTF8(), e);
            throw new RuntimeException(e.contentUTF8(), e);
        }
        return order.getId();
    }

2. AP模式

a) 理论
AT模式同样是分阶段提交的事务模型,它在XA模型的基础上,解决了分支事务执行完毕后资源锁定周期过长的缺陷。
在这里插入图片描述在XA模式的基础上,当分支事务执行sql 后,不进行等待。而是记录更新前后的快照(undolog),然后直接提交事务。当提交全局事务时,删除undolog 文件,当需要回滚时,根据undolog 恢复数据到更新前。

例如: 执行一个分支业务:update tb_account set money=monry-50 where id=1
在这里插入图片描述

b) AT模式的脏写问题
当一个分支事务完成sql 业务后,就会释放资源锁,此时,其他业务就能拿到资源锁,再次对资源进行修改操作。此时,如果第一个业务需要回滚,就会根据其记录的undolog 文件进行数据恢复,但是在此之后的其他事务的修改就无法生效了。

AT模式引入全局锁来解决此问题。

全局锁: 由TC 记录当前正在操作某行数据的事务,该事务持有全局锁,具有执行权。

此时,每个分支事务执行完需要提交时,需要先获取全局锁。该全局锁只有等待全局事务执行完毕后,才会释放。因此,如果此时存在其他分支事务需要修改该资源时,同样需要获取全局锁,当然,如果前一个分支事务的全局事务没有执行完时,是无法获取到此全局锁的。因此,解决了AT模式的脏读问题。

但是,全局锁又引出一个新的问题。如果两个事务,同时对一个资源,一个获取到了全局锁,另一个获取到了资源锁。双方都在等待对方释放锁,那么岂不是会出现死锁问题?
:事实是,全局锁的优先级高于资源锁。当一个事务获取到资源锁而无法获取全局锁时,它会重试几次后,释放资源锁。防止出现死锁问题。

c) 实现
AT模式需要一个表来记录全局锁(这个表是与TC其它表放在一起的)、另一张表来记录数据快照undo_log(这张表是生成快照,与具体业务放在一起的)。

全局锁的表

	-- ----------------------------
	-- 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;

undolog的表

	-- ----------------------------
	-- 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;
	
	-- ----------------------------
	-- Records of undo_log
	-- ----------------------------

此时的数据库表如下所示:

在这里插入图片描述

Seata 的 starter 同样完成了AT 模式的自动装配,实现非常简单,步骤如下:
① 修改 application.yml 文件(每个参与事务的微服务都需要进行配置),开启AT模式:

seata:
  data-source-proxy-mode: AT # 开启数据源代理的XA模式

② 给发起事务的入口方法添加 @GlobalTransactional 注解:

    @Override
//    @Transactional
    @GlobalTransactional
    public Long create(Order order) {
        // 创建订单
        orderMapper.insert(order);
        try {
            // 扣用户余额
            accountClient.deduct(order.getUserId(), order.getMoney());
            // 扣库存
            storageClient.deduct(order.getCommodityCode(), order.getCount());

        } catch (FeignException e) {
            log.error("下单失败,原因:{}", e.contentUTF8(), e);
            throw new RuntimeException(e.contentUTF8(), e);
        }
        return order.getId();
    }

AT模式的优点:

  • 分支事务执行完毕后,直接提交事务,释放数据库资源,性能较好;
  • 利用全局锁实现读写隔离;
  • 没有代码侵入,框架自动完成回滚和提交;

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致;
  • 框架的快照功能会影响性能,但比XA模式好很多。

3. TCC模式

a) 理论
TCC模式在AT模式的基础上,抛弃了使用快照来记录数据的方式,采用人工编码来实现数据的恢复。需要实现三个方法:

  • Try:检测资源相关属性是否能够支撑接下来的业务(账户金额是否足够)并预留。
  • Confirm:完成资源的操作;
  • Cancel:释放预留资源;try 的反向操作。

例如: 一个扣除用户余额的业务。假设账户A原来的余额是100,需要金额扣除30元;

  • Try阶段:检查余额是否充足,如果充足则冻结金额增加30元,可用金额扣除30元;
    在这里插入图片描述

  • Commit阶段:如果需要提交事务,则冻结金额扣除30
    在这里插入图片描述

  • Cancel阶段:如果需要回滚,则冻结金额扣除30,可用余额增加30
    在这里插入图片描述

TCC模式是采用资源预留的方式,实现事务回滚时的数据恢复。

TCC的工作模型图:
在这里插入图片描述
b) TCC的空回滚与业务悬挂

当某分支事务的try 阶段阻塞时,可能导致全局事务超时而触发所有分支事务的 cancel 操作。在未执行try 操作时,先执行cancel操作,此时,cancel不能做回滚,这就是空回滚

对于已经空回滚的业务,如果以后继续执行try,就永远无法执行confirm 或 cancel 操作,这就是业务悬挂。应当阻止执行空回滚后的try 操作,避免悬挂。

在这里插入图片描述
c) 实现

① 数据库建表
为了实现空回滚、防止业务喧哗,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id 和 执行状态。因此,需要添加一张表:

	CREATE TABLE `account_freeze_tbl` (
	  `xid` varchar(128) NOT NULL,
	  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
	  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
	  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
	  PRIMARY KEY (`xid`) USING BTREE
	) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

其中:

  • xid:是全局事务id
  • freeze_money:用来记录用户冻结金额
  • state:用来记录事务状态

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解声明,语法如下:
在这里插入图片描述
具体代码如下:
接口

	@LocalTCC
	public interface AccountTCCService {
	
	    @TwoPhaseBusinessAction(name = "deduct",commitMethod = "confirm",rollbackMethod = "cancel")
	    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
	                @BusinessActionContextParameter(paramName = "money") int money);
	
	    boolean confirm(BusinessActionContext ctx);
	
	    boolean cancel(BusinessActionContext ctx);
	}

实现类

	@Slf4j
	@Service
	public class AccountTCCServiceImpl implements AccountTCCService {
	    @Autowired
	    private AccountMapper accountMapper;
	
	    @Autowired
	    private AccountFreezeMapper accountFreezeMapper;
	
	    @Override
	    @Transactional
	    public void deduct(String userId, int money) {
	        // 0. 检测金额是否充足
	        // 由于数据库的余额字段设为 unsigned,因此可以跳过用户的检测余额是否充足的步骤
	
	        // 获取事务ID
	        String xid = RootContext.getXID();
	
	        // 判断freeze 中是否有冻结记录。用来避免业务悬挂问题。(已经空回滚了,再执行try)
	        AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
	        if(oldFreeze != null){
	            // CANSEL已经执行过了,拒绝此次业务
	            return;
	        }
	
	        // 1. 扣除可用余额
	        accountMapper.deduct(userId,money);
	
	        // 2. 记录冻结金额,事务状态。
	        AccountFreeze freeze = new AccountFreeze();
	        freeze.setUserId(userId);
	        freeze.setFreezeMoney(money);
	        freeze.setState(AccountFreeze.State.TRY);
	        freeze.setXid(xid);
	
	        accountFreezeMapper.insert(freeze);
	    }
	
	    @Override
	    public boolean confirm(BusinessActionContext ctx) {
	        // 1. 获取事务ID
	        String xid = ctx.getXid();
	
	        // 2. 根据id 删除冻结记录
	        int count = accountFreezeMapper.deleteById(xid);
	
	        return count == 1;
	    }
	
	    @Override
	    public boolean cancel(BusinessActionContext ctx) {
	        // 0. 查询冻结记录
	        String xid = ctx.getXid();
	        AccountFreeze freeze = accountFreezeMapper.selectById(xid);
	
	        // 判断是否为空回滚
	        if(freeze == null){
	            freeze = new AccountFreeze();
	            freeze.setUserId(ctx.getActionContext("userId").toString());
	            freeze.setFreezeMoney(0);
	            freeze.setState(AccountFreeze.State.CANCEL);
	            accountFreezeMapper.insert(freeze);
	
	            return true;
	        }
	
	        // 幂等判断
	        if(freeze.getState() == AccountFreeze.State.CANCEL){
	            // 已经处理过一次CANCEL了,无需重复处理
	            return true;
	        }
	
	        // 1. 恢复可用余额
	        String userId = freeze.getUserId();
	        int money = freeze.getFreezeMoney();
	        accountMapper.refund(userId,money);
	
	        // 2. 将冻结金额清零,将状态改为cancel
	        freeze.setFreezeMoney(0);
	        freeze.setState(AccountFreeze.State.CANCEL);
	
	        int count = accountFreezeMapper.updateById(freeze);
	
	        return count == 1;
	    }

		@Override
	    public boolean cancel(BusinessActionContext ctx) {
	        // 0. 查询冻结记录
	        String xid = ctx.getXid();
	        AccountFreeze freeze = accountFreezeMapper.selectById(xid);
	
	        // 判断是否为空回滚
	        if(freeze == null){
	            freeze = new AccountFreeze();
	            freeze.setUserId(ctx.getActionContext("userId").toString());
	            freeze.setFreezeMoney(0);
	            freeze.setState(AccountFreeze.State.CANCEL);
	            accountFreezeMapper.insert(freeze);
	
	            return true;
	        }
	
	        // 幂等判断
	        if(freeze.getState() == AccountFreeze.State.CANCEL){
	            // 已经处理过一次CANCEL了,无需重复处理
	            return true;
	        }
	
	        // 1. 恢复可用余额
	        String userId = freeze.getUserId();
	        int money = freeze.getFreezeMoney();
	        accountMapper.refund(userId,money);
	
	        // 2. 将冻结金额清零,将状态改为cancel
	        freeze.setFreezeMoney(0);
	        freeze.setState(AccountFreeze.State.CANCEL);
	
	        int count = accountFreezeMapper.updateById(freeze);
	
	        return count == 1;
	    }
}

TCC的优点:

  • 分支事务直接完成事务的提交,释放数据库资源,性能好;
  • 相比AT模式,无需生成中间快照,无需使用全局锁,性能最强;
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库(如Redis等)

TCC的缺点:

  • 代码入侵,需要认为编写try、Confirm 和 Cancel 接口。
  • 软状态,数据是最终一致性;
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理(一个事务的一次操作与多次操作结果是一样的)

4. SAGA模式

对于Saga模式,这里只做简单的介绍。

Saga模式是SEATA提供的长事务解决方案,也分为两个阶段:

  • 一阶段:直接提交本地事务;
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚。

在这里插入图片描述

Saga模式优点

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高;
  • 一阶段可以直接提交事务,无锁,性能好;
  • 不用编写TCC中的三个阶段,实现简单;

Saga模式优点

  • 软状态持续事件不确定,时效性差;
  • 没有锁,没有事务隔离,会存在脏写操作;

5. 四种模式对比

在这里插入图片描述


以上就为本篇文章的全部内容啦!

如果本篇内容对您有帮助的话,请多多点赞支持一下呗!