【分布式事务】深入探索 Seata 的四种分布式事务解决方案的原理,优缺点以及在微服务中的实现

news2024/11/26 14:53:45

文章目录

  • 前言
  • 一、XA 模式
    • 1.1 XA 模式原理
    • 1.2 XA 模式的优缺点及应用场景
    • 1.3 Seata XA 模式在微服务中的实现
  • 二、AT 模式
    • 2.1 Seata AT 模式原理
    • 2.2 AT 模式的脏写问题和写隔离
    • 3.3 AT 模式的优缺点
    • 3.4 Seata AT 模式在微服务中的实现
  • 三、TCC 模式
    • 3.1 TCC 模式原理
    • 3.2 Seata 的 TCC 模型
    • 3.3 TCC 模型事务悬挂和空回滚
    • 3.4 TCC 模式的实现
  • 四、SAGA 模式
    • 4.1 SAGA 模式原理
    • 4.2 SAGA 模式的优缺点


前言

分布式事务是在分布式系统中保持数据一致性的关键问题之一。Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,它提供了四种不同的事务模式,分别是XA、AT、TCC、SAGA。本文将深入探讨这四种分布式事务解决方案的原理、优缺点以及应用场景,以帮助开发人员更好地选择适合其项目的分布式事务模式。

一、XA 模式

1.1 XA 模式原理

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。

XA 模式的事务管理可以分为两个阶段:

正常情况,即所有分支事务都执行成功,需要提交:

  1. 第一阶段: 事务协调者(TM)通知局部资源管理器(RM)准备执行业务操作。所有分支事务执行成功,RM 告知 TM 自己已经就绪准备提交事务。

  2. 第二阶段: TM 通知所有 RM 提交事务,即所有分支事务的更改被持久化,当所有的分支事务提交成功后告知 TM 自己已提交。

异常情况,即存在分支事务执行失败,需要回滚:

  1. 第一阶段: 事务协调者(TM)通知局部资源管理器(RM)准备执行业务操作,存在分支事务执行失败,就向 TM 返回自己执行失败。

  2. 第二阶段: 如果存在分支事务执行失败,TM 通知所有执行成功并处于就绪状态的 RM 回滚事务,即所有分支事务的更改被撤销。

Seata 的 XA 模式

Seata 对原始的 XA 模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图如下:


Seata 的 XA 模式与传统的 XA 模式相似,在实际工作流程中进行了一些调整。以下是 Seata XA 模式的工作原理说明:

  1. RM 一阶段的工作:

    • RM 将分支事务注册到全局事务协调器(TC)。
    • RM 执行分支业务操作,但不提交。这确保了分支事务不会立即生效,等待全局协调后再决定是否提交。
    • RM 报告分支事务执行状态给 TC。
  2. TC 二阶段的工作:

    • TC 检测各个分支事务的执行状态。
    • 如果所有分支事务都成功,TC通知所有RM提交事务,即提交所有分支事务的更改。
    • 如果有任何分支事务失败,TC通知所有RM回滚事务,即回滚所有分支事务的更改
  3. RM二阶段的工作:

    • RM 接收来自TC的指令,根据指令来提交或回滚分支事务。

Seata 的 XA 模式通过全局事务协调器(TC)确保分布式事务的一致性,它与传统 XA 模式的主要区别在于 Seata 对分支事务的执行进行了微调,以便更好地适应分布式事务管理的需求。这种模式允许应用在分布式环境中实现事务管理,确保数据的一致性。

1.2 XA 模式的优缺点及应用场景

优点:

  1. 强一致性: XA 模式保证了数据的强一致性,即要么所有事务都提交成功,要么都回滚失败。这对于需要高度一致性的应用非常重要,如金融系统或在线支付。

  2. 广泛支持: XA 模式是一种经典的分布式事务处理标准,几乎所有主流的数据库都支持 XA 规范,因此可以在不同的数据库和消息队列之间进行事务协调。

缺点:

  1. 性能开销: 由于涉及到两个阶段的协调,XA 模式通常性能开销较大。在第一阶段,需要等待所有分支事务执行完毕并报告状态,而在第二阶段,需要等待所有分支事务的提交或回滚完成。这导致了较长的事务执行时间。

  2. 单点问题: XA 模式中需要一个全局的事务协调器(Transaction Coordinator),这可能成为系统的单点故障。如果事务协调器发生故障,整个系统的可用性将受到影响。

  3. 资源锁定: 在第一阶段,分支事务执行后需要等待全局事务协调器的指令,这可能导致资源锁定时间较长,影响并发性能。

  4. 复杂性: 实现 XA 模式的分布式事务处理需要复杂的编程和配置,开发和维护成本较高。

应用场景:

XA 模式适用于对数据强一致性要求较高的场景,其中数据的一致性比性能更为重要。一些典型的应用场景包括:

  • 金融系统:在金融交易中,数据的一致性至关重要,因此需要使用 XA 模式来确保所有相关操作的一致性。
  • 在线支付:在线支付系统必须确保交易的一致性,以避免出现重复扣款或未扣款的情况。
  • 订单处理系统:在订单处理系统中,需要确保订单的创建、支付、发货等操作都能够保持一致性,以避免出现订单漏发或重复发货的问题。

总之,XA 模式适用于那些对数据一致性要求非常高且可以承受一定性能开销的分布式应用场景。

1.3 Seata XA 模式在微服务中的实现

这里我以前文的 seata-cloud-demo 为例,来演示 Seata 分布式事务不同的解决方案,首先是 XA 模式:

由于 Seata 的 starter 已经完成了 XA 模式的自动装配,实现就变得非常简单了,步骤如下:

  1. 修改application.yml文件(每个参与事务的微服务),开启 XA 模式:

    seata:
      data-source-proxy-mode: XA # 开启数据源代理的 XA 模式
    
  2. 给发起全局事务的入口方法添加 @GlobalTransactional 注解,在上面创建订单的整个微服务中,全局事务的入口就是创建订单的业务逻辑的 create 方法:

    这里将 @Transactional 注解改成 @GlobalTransactional 即可:

  3. 当完成上面所有的配置之后,就可以启动所有的微服务,然后使用 Postman 进行测试了:

    • 现在账户表和库存表的数据如下:

    • 直接演示创建订单失败的情况:
    • 此时由于库存不足,创建订单失败了,在前文中因为没有实现分布式事务,因此账户服务在执行成功之后没有进行回滚,而现在实现了分布式事务,则就不应该再扣款了:

      通过 account-service 服务的日志来看,也是先执行成功了,最后因为库存服务执行失败,进行了回滚操作:

二、AT 模式

AT 模式同样是分阶段提交的事务模式,不过弥补了 XA 模型中因为锁对资源而导致的周期过长问题:

2.1 Seata AT 模式原理

AT模式(Auto Transaction)是一种自动事务模式,它试图通过自动提交和回滚来实现分布式事务的一致性。与 XA 模式不同,AT 模式一阶段直接提交事务,不会锁定资源,AT 模式下数据的回滚依赖于数据库的 undo log

在 Seata 的 AT 模式中,事务管理的架构图如下:

Seata AT 模式的事务管理流程:

  1. 阶段一 RM 的工作:

    • 注册分支事务:当一个分支事务开始时,它会在本地数据库中注册自己。
    • 记录 undo-log(数据快照):在分支事务执行业务SQL之前,记录当前数据的快照,以便在回滚时恢复数据。
    • 执行业务SQL并提交:分支事务执行业务SQL并自动提交本地事务。这是AT模式的特点,不涉及两阶段提交。
    • 报告事务状态:RM报告分支事务的执行状态给TC(Transaction Coordinator)。
  2. 阶段二提交时 RM 的工作:

    • 在提交时,RM只需删除相应的undo-log,因为分支事务已经自动提交,不需要额外的提交操作。
  3. 阶段二回滚时 RM 的工作:

    • 在回滚时,RM根据之前记录的undo-log来恢复数据到更新前的状态。

Seata 的 AT 模式依赖于数据库的本地事务管理能力,每个分支事务在本地自动管理自己的事务。如果分支事务成功提交,那么全局事务也会提交;如果分支事务出现失败,全局事务将会回滚,确保数据的一致性。

总的来说,Seata 的 AT 模式通过自动提交和回滚分支事务,减少了全局事务协调的复杂性,同时提供了高性能的分布式事务管理方式。这种模式适用于那些可以容忍一定程度数据不一致的应用场景,特别是对性能要求较高的应用。

例如,现在用一个真实的业务来梳理下 AT 模式的原理。

比如,现在又一个数据库表,记录用户余额:

idmoney
1100

其中一个分支业务要执行的SQL为:

update tb_account set money = money - 10 where id = 1

AT 模式下,当前分支事务执行流程如下:

一阶段:

  1. TM 发起并注册全局事务到 TC

  2. TM 调用分支事务

  3. 分支事务准备执行业务 SQL

  4. RM 拦截业务 SQL,根据 where 条件查询原始数据,形成快照。

    {
        "id": 1, "money": 100
    }
    
  5. RM 执行业务 SQL,提交本地事务,释放数据库锁。此时 money = 90

  6. RM 报告本地事务状态给 TC

二阶段:

  1. TM 通知 TC 事务结束

  2. TC 检查分支事务状态

    • 如果都成功,则立即删除快照
    • 如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为 100

上述过程的流程图:

2.2 AT 模式的脏写问题和写隔离

脏写问题:

在多线程并发访问 AT 模式的分布式事务时,有可能出现脏写问题,例如下图所示:


对上图中脏写问题的说明:

  1. 同时有两个并发的全局事务执行:update tb_account set money = money - 10 where id = 1
  2. 事务 1 首先获取到了 DB 锁,保存了快照(此时 money 为 100)之后,执行业务逻辑将 money 设置为了 90,然后提交了分支事务,并是否了 DB 锁;
  3. 此时,事务 2 也获取到了 DB 锁,然后保存了快照(此时的 money 为 90),然后执行业务将 money 设置成了 80,提交分支事务并是否 DB 锁;
  4. 事务 1 因为后续的其中分支事务执行失败了,因此需要恢复前面所有提交了的分支事务,此时再次获取到了 DB 锁,然后需要根据自己的快照(money 为 100)来恢复数据。但是,此时的数据库中的 money 为 80,而站在事务 1 的角度则应该为 90,因此就丢失了一次更新,出现了脏写的问题。

解决方法:

为了解决上面的脏写问题,Seata 引入了全局锁,开启写隔离。即在释放 DB 锁之前,先拿到全局锁,避免同一时刻有另外一个事务来操作当前数据:

对上述写隔离的详细说明:

  1. 同样是两个并发的全局事务执行:update tb_account set money = money - 10 where id = 1
  2. 事务1 首先获取到 DB 锁,并保存了快照(money 为 100),然后执行了业务逻辑(更新 money 为 90),在提交分支事务之前获取了全局锁,然后提交分支事务,释放 DB 锁;
  3. 然后事务 2 就获取到了 DB 锁,保存快照(money 为 90),然后执行业务逻辑(设置money 为 80),执行完之后尝试获取全局锁,但此时全局锁被事务 1 持有,因此执行重复尝试获取,如果一直获取不到则会导致任务超时,自动回滚并释放 DB 锁;
  4. 后面事务1因为其他分支事务执行失败,首先获取 DB 锁,然后根据快照恢复数据,快照中 money 为 100,实际和 事务1 所期望的 money 都为 90,因此成功通过快照恢复数据。

要实现上面这个功能,需要额外引入一个数据库表,来记录当前全局锁的获取情况。

但是还是一个问题,那就是如果是一个 Seata 管理的全局事务和一个非 Seata 管理的全局事务来并发执行上述的业务逻辑,又该如何保存数据的一致性呢?

幸运的是,Seata 以及为我们考虑到了这个问题(虽然这个问题一般不会出现):
Seata 会在执行业务逻辑的前后都生成一个快照,即before-imageafter-image,后面当需要进行数据的恢复时就需要对比这两个快照和实际数据库中的数据是否一致,然后再执行数据的恢复操作。如果数据不一致,则需要记录异常,发出警告,通知人工介入来处理。

3.3 AT 模式的优缺点

AT(Auto Transaction)模式是Seata中的一种分布式事务模式,它具有优点和缺点如下:

优点:

  1. 一阶段完成直接提交事务: AT 模式的第一阶段(执行业务SQL并自动提交)不涉及全局事务协调器的参与,因此可以更快地完成事务操作,释放数据库资源,从而提高性能。

  2. 利用全局锁实现读写隔离: AT 模式引入全局锁和写隔离,可以防止脏写问题和提供写隔离。这有助于确保数据的一致性和避免并发写操作导致的问题。

  3. 没有代码侵入,框架自动完成回滚和提交: 在 AT 模式下,开发人员不需要编写特定的代码来处理分布式事务的提交和回滚,Seata 框架会自动处理这些事务管理的细节,降低了开发的复杂性。

缺点:

  • 两阶段之间属于软状态: AT 模式中的两阶段提交之间是一个软状态,全局事务在第一阶段已经提交了分支事务,但最终一致性要在第二阶段进行检查和修复。这可能导致一定程度的数据不一致性,尤其是在第二阶段出现问题时。

尽管 AT 模式具有一些显著的优点,例如性能较好和较低的代码侵入性,但它仍然面临着最终一致性和软状态的挑战。开发人员需要权衡这些因素,并根据应用程序的需求选择适当的分布式事务模式。

3.4 Seata AT 模式在微服务中的实现

AT 模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。只不过,AT 模式需要一个表来记录全局锁,另一张表来记录数据快照 undo_log。下面是实现 AT 模式的具体步骤:

  1. 导入数据库表,记录全局锁和快照

lock_table 用于记录全局锁,其使用对象的 seata-server,因此需要在seata数据库(由seataServer.properties配置文件指定)中创建该表:

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

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;
  1. 修改所有微服务的application.yml文件,将事务模式修改为 AT 模式:
seata:
  data-source-proxy-mode: AT # 开启数据源代理的 AT 模式

可以发现,AT 模式是 Seata 的默认模式:

  1. 重启微服务并测试

此时,以断点调试的方式发送下面的请求:

断点设置:

启动调试,可以看到在lock_table 表中为账户表和订单表都创建了全局锁:

并且 undo_log表中也创建了相应的快照:

放行之后,通过查看 account-service 服务的日志:

发现成功使用快照对数据进行了恢复,并且将lock_tableundo_log 表中的数据也进行了删除。

三、TCC 模式

3.1 TCC 模式原理

TCC模式(Try-Confirm-Cancel)与AT模式非常相似,它也是一种分布式事务模式,但不同之处在于TCC模式需要通过人工编码来实现数据恢复。在TCC模式中,需要实现三个方法:Try(尝试)、Confirm(确认)、Cancel(取消)。下面将详细介绍TCC模式的原理和流程。

TCC模式的三个方法:

  1. Try(尝试): 这个方法用于资源的检测和预留。在Try阶段,事务会尝试执行业务逻辑,检查是否有足够的资源来执行操作,并预留资源以确保后续的确认或取消操作可以成功执行。

  2. Confirm(确认): 这个方法用于完成资源操作业务。如果Try成功,那么在确认阶段将完成资源操作的实际业务。确认操作一定要能成功,以确保资源的变化被永久性地提交。

  3. Cancel(取消): 这个方法用于资源的释放和恢复。如果Try成功但后续的确认操作出现问题,那么需要执行取消操作来释放预留的资源并恢复原始状态。

流程分析:

让我们通过一个简单的例子来说明TCC模式的流程,假设有一个扣减用户余额的业务。账户A的初始余额是100元,需要扣减30元。

阶段一(Try):

在Try阶段,我们需要检查账户余额是否充足,如果充足,就会增加冻结金额并扣除可用余额。初始余额如下:

初始余额

余额充足,可以冻结30元:

冻结金额增加

此时,总金额 = 冻结金额 + 可用金额,数量依然是100元,事务可以直接提交而无需等待其他事务。

阶段二(Confirm):

如果要提交(Confirm),则需要扣减冻结金额30元,确认可以提交。不过之前的可用金额已经扣减过了,这里只需清除冻结金额:

冻结金额清除

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元。

阶段三(Cancel):

如果要回滚(Cancel),则需要扣减冻结金额30元,同时恢复可用余额30元,以释放冻结金额并恢复可用金额:

冻结金额扣减和可用金额增加

TCC模式的关键在于实现Try、Confirm和Cancel方法,以确保事务的一致性和可恢复性。如果Try成功但Confirm或Cancel出现问题,需要能够正确处理资源的释放和恢复操作。

3.2 Seata 的 TCC 模型

Seata中的TCC(Try-Confirm-Cancel)模型延续了之前介绍的事务架构,如下图所示:

Seata TCC模型

TCC模式中,每个阶段的作用如下:

  • Try(尝试): 这个阶段用于资源检查和预留。事务会尝试执行业务逻辑,检查资源是否足够,并预留资源。

  • Confirm(确认): 这个阶段用于完成资源操作业务。如果Try成功,Confirm将执行实际的资源操作并提交事务。

  • Cancel(取消): 这个阶段用于资源释放和恢复。如果Try成功但Confirm出现问题,需要执行Cancel来释放资源并恢复原始状态。

TCC模式的优点包括:

  • 一阶段完成直接提交事务,释放数据库资源,性能好: TCC的Try阶段可以迅速完成,无需等待其他事务,从而释放数据库资源,提高性能。

  • 相比AT模式,无需生成快照,无需使用全局锁,性能最强: TCC模式相对于AT模式,减少了对数据库快照的依赖,同时也不需要全局锁,因此具有较高的性能。

  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库: TCC模式不要求数据库必须支持XA事务,因此可以用于非事务型数据库。

TCC模式的缺点包括:

  • 有代码侵入,需要人为编写Try、Confirm和Cancel接口,较为繁琐: TCC模式需要开发人员编写特定的Try、Confirm和Cancel方法,这可能增加了开发的复杂性。

  • 软状态,事务是最终一致: TCC模式的事务具有软状态,确认和取消操作可能出现问题,需要考虑如何处理失败情况以保证最终一致性。

总之,TCC模式在性能方面表现出色,但需要开发人员编写更多的代码来处理分布式事务的逻辑。选择TCC模式还应该考虑应用程序的特定需求和数据库的支持情况。

3.3 TCC 模型事务悬挂和空回滚

在TCC(Try-Confirm-Cancel)模型中,存在两个重要问题,即事务悬挂和空回滚,它们涉及到事务的正确执行和终止。

  1. 空回滚:

如下图所示:

当某分支事务的Try阶段被阻塞,可在这里插入代码片能导致全局事务超时,触发二阶段的 Cancel 操作。然而,在执行Cancel操作之前,Try操作尚未执行,这种情况被称为空回滚。这意味着 Cancel 操作在 Try 操作之前执行,导致 Cancel 操作无法正常回滚已经执行的 Try 操作。空回滚是一个问题,因为它会导致事务的状态不正确。

解决空回滚的方法是在执行 Cancel 操作时,应判断 Try 是否已经执行。如果 Try 尚未执行,那么 Cancel 操作应该记录当前的状态,以等待进行 Try 操作时判断这个操作是否是 Cancel。

  1. 业务悬挂:

事务悬挂是在发生空回滚后的情况下出现的问题。当某个业务的 Try 操作被阻塞,已经发生了空回滚,然后之前被阻塞的 Try 操作重新启动并继续执行时,这些 Try 操作可能永远无法完成 Confirm 或 Cancel 操作,导致事务一直处于中间状态,这就是事务悬挂。

解决事务悬挂的方法是在执行 Try 操作时,判断是否已经执行了 Cancel 操作。如果 Cancel 操作已经执行,那么 Try 操作应该被阻止,以避免事务悬挂的问题。

3.4 TCC 模式的实现

TCC(Try-Confirm-Cancel)模式是一种用于实现分布式事务的模式,它通过分解事务操作为三个步骤,分别是Try、Confirm、Cancel,来实现分布式事务的一致性和可靠性。这种模式通常需要解决空回滚和业务悬挂等问题。以下是TCC模式的实现示例:

  1. 数据库表设计

为了解决TCC模式中的空回滚和业务悬挂问题,通常需要使用数据库表来记录事务状态。以下是一个示例数据库表的设计:

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:全局事务的唯一标识符。
  • user_id:用户ID,标识参与事务的用户。
  • freeze_money:记录用户的冻结金额,用于TCC事务。
  • state:记录事务状态,通常使用0表示Try阶段,1表示Confirm阶段,2表示Cancel阶段。

通过这张表,可以追踪每个全局事务的状态,从而解决空回滚和业务悬挂问题。

  1. 声明TCC接口

TCC模式的 Try、Confirm、Cancel 方法都需要在接口中基于注解进行声明。在service包下创建一个接口,并声明 TCC 的三个方法:

@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 context);

    boolean cancel(BusinessActionContext context);
}
  1. 编写实现类

然后,在account-service服务的service.impl包下创建一个类,实现 TCC 业务:


@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper accountFreezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0. 获取事务id
        String xid = RootContext.getXID();
        // 1. 处理业务悬挂,判断 freeze 中是否有冻结记录,如果有,一定是执行过 CANCEL,因此拒绝业务
        AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
        if(oldFreeze != null){
            // 执行过 CANCEL,因此拒绝业务
            return;
        }

        // 2. (由于是 unsigned 类型)直接扣减可用余额
        accountMapper.deduct(userId, money);
        // 3. 记录冻结金额,事务状态
        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 context) {
        // 1. 获取事务 id
        String xid = context.getXid();
        // 2. 根据id删除冻结记录
        int count = accountFreezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // 0. 查询冻结记录
        String xid = context.getXid();
        String userId = (String) context.getActionContext("userId");
        AccountFreeze freeze = accountFreezeMapper.selectById(xid);

        // 1. 空回滚判断,判断 freeze 是否为 null
        if (freeze == null) {
            // 为 null 则证明 try 没有执行,需要空回滚
            freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            accountFreezeMapper.insert(freeze);
            return true;
        }

        // 2. 幂等判断
        if(freeze.getState() == AccountFreeze.State.CANCEL){
            // 以及处理过一次 CANCEL 操作
            return true;
        }

        // 3. 恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 4. 将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = accountFreezeMapper.updateById(freeze);
        return count == 1;
    }
}

这里,首先声明了TCC的Try、Confirm、Cancel方法,并在实现类中编写了相应的逻辑。在Try方法中,我们扣减可用余额并记录冻结金额和事务状态。在Confirm方法中,我们根据事务ID删除冻结记录。在Cancel方法中,我们恢复可用余额,将冻结金额清零,并将事务状态改为CANCEL。这些操作将确保TCC事务的正常执行,即使出现空回滚或业务悬挂情况也能进行处理。

四、SAGA 模式

4.1 SAGA 模式原理

SAGA 模式是一种分布式事务处理模式,旨在解决大规模分布式系统中的事务问题。其原理如下:

  1. 事务分解:将一个复杂的分布式事务分解为多个小事务,每个小事务可以独立执行。这些小事务可以跨越不同的服务和系统。

  2. 事务状态迁移:每个小事务有两个关键操作,即compensating(回滚)和confirming(确认)。事务状态可以从一个状态迁移到另一个状态,例如从Started(已开始)到Completed(已完成),或从StartedCompensated(已回滚)。

  3. 协调器:有一个中央协调器或协调服务来管理整个事务的执行。协调器负责确保事务的状态迁移按正确的顺序发生,以保持事务的一致性。

  4. 异常处理:如果某个小事务失败,协调器会触发相应的compensating操作来撤销之前已经执行的小事务,以确保事务的一致性。

  5. 事务状态管理:协调器跟踪和管理每个小事务的状态变化,以确保最终事务能够成功完成或者回滚到一致的状态。

  6. 最终一致性:SAGA 模式追求最终一致性,即事务可能会暂时处于不一致状态,但最终会达到一致状态。这意味着SAGA模式在处理大规模分布式事务时牺牲了一些一致性,以提高性能和可伸缩性。

4.2 SAGA 模式的优缺点

优点:

  1. 分布式事务处理:SAGA 模式是专门用于处理分布式事务的模式,能够有效解决跨多个服务的复杂事务问题。

  2. 高可伸缩性:由于事务被分解成多个小事务,这些小事务可以并行执行,提高了系统的可伸缩性。

  3. 容错性:SAGA 模式可以容忍某些小事务失败,通过回滚操作来保持整体事务的一致性。

  4. 降低锁竞争:与传统的两阶段提交(2PC)相比,SAGA 模式可以减少全局锁的使用,从而降低锁竞争。

  5. 灵活性:SAGA 模式允许在事务的执行过程中动态调整事务的状态迁移路径,更加灵活适应不同的业务需求。

缺点:

  1. 复杂性:SAGA 模式的实现相对复杂,需要编写和维护compensatingconfirming操作,以及确保正确的状态迁移。

  2. 最终一致性:SAGA 模式只追求最终一致性,可能会在事务过程中出现一时的不一致,需要额外的处理来处理这些不一致。

  3. 性能开销:由于SAGA 模式需要在协调器上执行状态管理和协调操作,可能会引入一些性能开销。

  4. 难以调试:SAGA 模式中的事务状态管理和回滚操作可能会增加调试的难度,特别是在复杂的分布式环境中。

总的来说,SAGA 模式适用于需要处理大规模分布式事务的场景,但在实施时需要权衡复杂性、一致性和性能之间的关系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1186321.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

洛谷 P1020 [NOIP1999 普及组] 导弹拦截【一题掌握三种方法:动态规划+贪心+二分】最长上升子序列LIS解法详解

P1020 [NOIP1999 普及组] 导弹拦截 前言题目题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示题目分析注意事项 代码动态规划(NOIP要求:时间复杂度O(n^2^))贪心二分(O(nlgn)) 后话额外测试用例样例输入 #1…

数据结构:Map和Set(2):相关OJ题目

目录 136. 只出现一次的数字 - 力扣(LeetCode) 771. 宝石与石头 - 力扣(LeetCode) 旧键盘 (20)__牛客网 (nowcoder.com) 138. 随机链表的复制 - 力扣(LeetCode) 692. 前K个高频单词 - 力扣&#xff08…

“百人专家团”背书 袋鼠妈妈“双十一”蓄势待发

从万千商家的角度来看,“双十一”实际上就是一场没有硝烟的“战争”,只有用心做产品的品牌才能成为常胜将军,要想在双十一脱颖而出在同品类榜单上占据有利位置,品牌力和产品力二者缺一不可。而专注母婴护肤10年的袋鼠妈妈品牌便是如此,从品牌诞生以来,始终专注母婴用户需求,打造…

STM32MPU6050角度的读取(STM32驱动MPU6050)

注:文末附STM32驱动MPU6050代码工程链接,需要的读者请自取。 一、MPU6050介绍 MPU6050是一款集成了三轴陀螺仪和三轴加速度计的传感器芯片,由英国飞利浦半导体(现为恩智浦半导体)公司生产。它通过电子接口&#xff08…

面包屑实现

背景:面包屑根据菜单内容显示不同内容。首页永远存在,后面的活动管理及多级菜单的面包屑展示。 实现原理: 通过this.$route.matched获取所有匹配路由,将处理首页外的其他路由设置到一个数组中,再通过数组循环方式显示…

Linux/centos上如何配置管理Web服务器?

Linux/centos上如何配置管理Web服务器? 1 Web简单了解2 关于Apache3 如何安装Apache服务器?3.1 Apache服务安装3.2 httpd服务的基本操作 4 如何配置Apache服务器?4.1 关于httpd.conf配置4.2 常用指令 5 简单实例 1 Web简单了解 Web服务器称为…

一篇文章让你Docker从入门到精通

一篇文章让你Docker从入门到精通 Docker简介docker的3要素docker安装--centos7示例docker底层原理docker常用命令docker镜像原理数据共享--容器数据卷数据卷容器dockerfile解析Dockerfile实战一 使用dockerfile构建ubuntu镜像,并在里面安装vim及网络工具 一张图展示…

2023 年微服务后端开发的 11 个最佳工具

前言 微服务架构以将复杂的应用程序分解为易管理的服务而闻名,然而,管理微服务是一项具有挑战性的任务。为了确保开发工作流程的高效性,需要采用特定的工具。 在本文中,小编将为您介绍2023年最热的11款后端微服务开发工具&#…

什么是CE认证?蓝牙耳机出口欧盟CE认证如何办理?CE-RED认证办理

蓝牙耳机是一种基于蓝牙技术的一种小型设备,只需要把这种轻巧的设备藏在耳机边而不需要直接使用通讯设备(手机、电脑等)就可以实现自由通话。蓝牙耳机就是将蓝牙技术应用在免持耳机上,让使用者可以免除恼人电线的牵绊,…

软件测试/测试开发丨接口自动化测试学习笔记,整体结构响应断言

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接:https://ceshiren.com/t/topic/27982 一、结构断言介绍 针对于“大响应数据”如何断言 针对主要且少量的业务断言其他字段不做数据正确性断言,只做类型与整体结构校验与前面…

【Ruoyi管理后台】用户登录强制修改密码

近期有个需求,就是需要调整Ruoyi管理后台:用户如果三个月(长时间)未修改过密码,需要在登录时强制修改密码,否则不能登录系统。 一、后端项目调整 从需求来看,我们需要在用户表增加一个字段,用于标记用户最…

转换流详解

问题引出:不同编码读取乱码问题 1、之前我们使用字符流读取中文是否有乱码? 没有的,因为创建字符流时如果不指定编码,那么使用的编码是jvm的默认编码,和文件编码都是UTF-8。 Creates a new FileReader, given the n…

MybatisPlus之新增操作并返回主键ID

在应用mybatisplus持久层框架的项目中&#xff0c;经常遇到执行新增操作后需要获取主键ID的场景&#xff0c;下面将分析及测试过程记录分享出来。 1、MybatisPlus新增方法 持久层新增方法源码如下&#xff1a; public interface BaseMapper<T> extends Mapper<T> …

RxJava/RxAndroid的操作符使用(二)

文章目录 一、创建操作1、基本创建2、快速创建2.1 empty2.2 never2.3 error2.4 from2.5 just 3、定时与延时创建操作3.1 defer3.2 timer3.3 interval3.4 intervalRange3.5 range3.6 repeat 二、过滤操作1、skip/skipLast2、debounce3、distinct——去重4、elementAt——获取指定…

xshell是什么软件,1000字让你完全了解xshell

很多从事开发或网络安全的人都或多或少知道xshell是什么软件&#xff0c;但是如果没有试用过的话可能对它的功能并不完全了解。今天小编就带你详细了解一下Xshell究竟是什么。 xshell是什么软件 一、xshell是什么软件 Xshell是一款功能强大的SSH&#xff08;Secure Shell&…

Jdk 1.8 for mac 详细安装教程(含版本切换)

Jdk 1.8 for mac 详细安装教程&#xff08;含版本切换&#xff09; 官网下载链接 https://www.oracle.com/cn/java/technologies/downloads/#java8-mac 一、选择我们需要安装的jdk版本&#xff0c;这里以jdk8为例&#xff0c;下载 macOS 版本&#xff0c;M芯片下载ARM64版本…

基于驾驶训练算法的无人机航迹规划-附代码

基于驾驶训练算法的无人机航迹规划 文章目录 基于驾驶训练算法的无人机航迹规划1.驾驶训练搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用驾驶训练算法来优化无人机航迹规划。 …

07 # 手写 find 方法

find 的使用 find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。 ele&#xff1a;表示数组中的每一个元素index&#xff1a;表示数据中元素的索引array&#xff1a;表示数组 <script>var arr [1, 3, 5, 7, 9];var result arr.find(fun…

SpringBoot加载测试类属性和配置说明

一、项目准备 1.创建项目 2.配置yml文件 test:name: FOREVERlove: sing二、测试类属性 1.Value 说明&#xff1a;读取yml中的数据。 package com.forever;import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.spr…

用中文编程工具编写的代码实现如图所示效果,请分享一下用你所学的编程语言写下这个代码,大家一起交流学习

用中文编程工具编写的代码实现如图所示效果&#xff0c;请分享一下用你所学的编程语言写下这个代码&#xff0c;大家一起交流学习 编程要求如图&#xff1a;在输入框里输入行数&#xff0c;随便输入多少&#xff0c;点击按钮&#xff0c;即刻显示如图所示效果&#xff0c;下一…