Seata(分布式事务)

1.分布式事务

1.1 本地事务

本地事务也被称为单机事务, 在传统数据库中, 本地事务必须满足4个原则ACID
在这里插入图片描述

1.2 分布式事务的概述

分布式事务存在多种情况, 例如:

  1. 跨服务的分布式事务
  2. 跨数据源的分布式事务
  3. 两者综合

案例: 跨数据源的分布式事务:
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:

  1. 创建订单
  2. 扣款
  3. 减库存

完成上面的操作需要访问三个不同的微服务和三个不同的数据库.
在这里插入图片描述

创建订单、扣款、减库存在每一个服务和数据库内是一个本地事务,可以保证ACID原则。但是当我们把三件事情看做一个整体的业务,要保证业务的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

2.案例演示

引入课前资料中提供的微服务

请添加图片描述

  • order-service:订单服务,负责管理订单。创建订单时,需要调用account-service和storage-service
  • account-service:账户服务,负责管理用户的资金账户。提供扣减余额的接口
  • storage-service:库存服务,负责管理商品库存。提供扣减库存的接口

在这里插入图片描述
测试发现,当库存不足时,如果余额已经扣减,并不会回滚,出现了分布式事务问题。


2.理论基础

解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。

2.1 CAP定理

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance (分区容错性)

一、一致性
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。

比如现在包含两个节点,其中的初始数据是一致的:
请添加图片描述
当我们修改其中一个节点的数据时,两者的数据产生了差异:请添加图片描述
要想保住一致性,就必须实现node01 到 node02的数据 同步:
请添加图片描述

二、可用性
Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。

如图,有三个节点的集群,访问任何一个都可以及时得到响应:请添加图片描述
当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:
请添加图片描述

三、分区容错
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
请添加图片描述
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

四、矛盾
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。

当节点接收到新的数据变更时,就会出现问题了:
请添加图片描述
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。

也就是说,在P一定会出现的情况下,A和C之间只能实现一个。


2.2 BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

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

2.3 解决分布式事务的思路

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

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

但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC):
请添加图片描述
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务


3.初识Seata

3.1 Seata的架构

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

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

整体的架构如图:
在这里插入图片描述

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

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

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


3.2 部署TC服务

参考课前资料提供的文档《 seata的部署和集成.md 》:请添加图片描述

部署成功后, 我们可以在nacos中看到这个seata服务
在这里插入图片描述


3.3 微服务集成Seata

3.3.1 引入依赖

首先,在order-service中引入依赖:

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

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <!--seata starter 采用1.4.2版本-->
    <version>${seata.version}</version>
</dependency>

3.3.2 配置TC地址

在order-service中的application.yml中,配置TC服务信息,通过注册中心nacos,结合服务名称获取TC地址:

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-tc-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组名称
  service:
    vgroup-mapping: # 事务组与cluster的映射关系
      seata-demo: SH

微服务如何根据这些配置寻找TC的地址呢?
我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

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

以上四个信息,在刚才的yaml文件中都能找到:
在这里插入图片描述
结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。

其它两个微服务也都参考order-service的步骤来做,完全一样。


4.Seata中的四种不同的事务模式

  • XA模式
  • AT模式
  • TCC模式
  • SAGA模式

4.1 XA模式

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

4.1.1 两个阶段

XA模式下将分布式事务定义为两个阶段:

  • 准备阶段(第一阶段)
  • 提交阶段(第二阶段)

一、准备阶段

  1. 事务协调者(TC)给每个参与者(资源管理器RM)发送Prepare消息, RM执行本地事务, 但并不提交, 整个过程中会持续锁住需要的资源
  2. RM在本地事务执行完成后, 将事务执行状态报告给事务协调者(TC),继续锁住需要的资源
    • 如果执行成功, 就返回就绪(ready)
    • 如果执行失败, 就返回失败(fail)

二、提交阶段

  • 事务协调者(TC)基于准备阶段所有RM返回的状态来判断下一步的操作
    • 如果所有RM都返回ready, 则通知所有事务参与者(RM), 提交事务
    • 如果其中一个RM返回fail, 则通知所有事务参与者(RM), 回滚事务

正常情况:
请添加图片描述

异常情况:
出异常的RM会抛异常, 所以就不需要回滚了…
请添加图片描述


4.1.2 Seata的XA模型

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

  • RM准备阶段的工作:
    • 将分支事务注册到TC
    • 执行分支业务sql, 但不提交
    • 将事务执行状态报告给TC
  • TC提交阶段的工作:
    • TC检测各分支事务执行状态
      • 如果都成功,通知所有RM提交事务
      • 如果有失败,通知所有RM回滚事务
  • RM提交阶段的工作:
    • 接收TC指令,提交或回滚事务

4.1.3 优缺点

XA模式的优点是什么?

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

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

4.1.4 实现XA模式

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

一、修改application.yml文件(每个参与事务的微服务),开启XA模式:
前提是已经在配置文件中配置了seate的服务地址

seata:
  data-source-proxy-mode: XA

二、给发起全局事务的入口方法添加@GlobalTransactional注解:

本例中是OrderServiceImpl中的create方法.请添加图片描述
  只要方法上加了@GlobalTransactional,Seata通过aop检测到之后,就会使用TM和TC通信,注册全局事务
  被@GlobalTransactiona涵盖的代码中, 不管是本服务中的sql操作,还是feign调用别的服务的sql操作,只要sql操作满足如下:insert操作,delete操作,update操作,select for update操作。 就会被seata增强,使用RM与TC通信,注册分支事务

三、重启order-service,再次测试,发现无论怎样,三个微服务都能成功回滚。


4.2 AT模式

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
AT模式支持的数据库有:MySQL、Oracle、PostgreSQL和 TiDB

4.2.1 Seata的AT模型

请添加图片描述

  • RM第一阶段的的工作:
    • 注册分支事务到TC, 让TC来协调管理所有分支事务
    • 拦截并解析正在执行的sql, 生成undo-log(数据快照)
      • 数据快照:如果你是在做update product set price = 20 where price=10 and id=1的操作
        那么undo_log中保存的就是反向sqlupdate product set price=10 where price=20 and id=1当然它的存储不会直接这么存,会经过处理。
    • 正常执行sql并提交
    • 向TC报告事务状态
  • TC第一阶段的工作:
    • TC检测各分支事务执行状态
      • 如果都成功,通知所有RM提交(RM已经提交过事务了, 所以此处的提交只是一个信号)
      • 如果有失败,通知所有RM回滚
  • RM第二阶段的工作
    • 根据TC的通知来进行操作
      • 如果是提交, 则删除undo-log即可
      • 如果是回滚, 则重启一个本地事务, 根据undo-log恢复数据到更新前

4.2.2 流程梳理

我们用一个真实的业务来梳理下AT模式的原理。比如,现在有一个数据库表,记录用户余额:

idmoney
1100

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

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

一阶段流程:

  1. TM发起并注册全局事务到TC
  2. TM调用分支事务
  3. 分支事务准备执行业务SQL
  4. RM拦截业务SQL,根据where条件查询原始数据,形成快照。
{
    "id": 1, "money": 100
}
  1. RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90
  2. RM报告本地事务状态给TC

二阶段流程

  1. TM通知TC事务结束
  2. TC检查分支事务状态
    • 如果都成功,则立即删除快照
    • 如果有分支事务失败,需要回滚。读取快照数据({“id”: 1, “money”: 100}),将快照恢复到数据库。此时数据库再次恢复为100

流程图:
请添加图片描述


4.2.3 AT与XA的区别

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致

4.2.4 脏写问题

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
在这里插入图片描述

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
在这里插入图片描述

  1. 事务1获取了全局锁, 记录事务1正在操作account表中所以为1的这条记录,(全局锁只有当阶段二结束了才会释放)
  2. 此时事务2也准备操作这条记录, 但无法获取全局锁, 所以一直等待事务1释放全局锁
  3. 而事务1此时收到TC的回滚通知, 需要获取DB锁(此处可以理解为行锁), 但是事务2也同时在操作这条记录, 所以就陷入了死锁了(事务2在等事务1的全局锁, 事务1等事务2的BD锁)
  4. 全局锁有更短的时间限制(30次重试, 间隔10秒), 3秒后无法获取全局锁, 任务超时, 事务2进行回滚, 释放BD锁
  5. 事务1获取BD锁, 根据快照恢复数据

简单来讲:在AT模式下, 当事务1操作account表的索引为1这条数据, 其他任何事务都无法操作这条数据, 因为获取不到全局锁, 只有当事务1所在的全局事务都结束了, 才会释放全局锁


4.2.5 优缺点

  • AT模式的优点:
    • 一阶段完成直接提交事务,释放数据库资源,性能比较好
    • 利用全局锁实现读写隔离
    • 没有代码侵入,框架自动完成回滚和提交
  • AT模式的缺点:
    • 两阶段之间属于软状态,属于最终一致
    • 框架的快照功能会影响性能,但比XA模式要好很多

4.2.6 实现AT模式

一、创建快照表、全局锁表
因为快照(undo_log)、全局锁(lock_table)都是保存在数据库里面的,所以需要我们创建同名的表来记录
在这里插入图片描述

  • undo_log表导入微服务关联的数据库
  • lock_table导入TC服务关联的数据库
    TC服务配置文件中配置的数据库, 因为全局锁是由TC维护和创建的
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;

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;

二、修改aoolication.yml
所有的事务参与者都要修改

seata:
  data-source-proxy-mode: AT # 默认就是AT

4.3 TCC模式

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作。

4.3.1 流程分析

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一(Try)
    检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30

    • 初始余额:请添加图片描述
    • 余额充足,可以冻结:请添加图片描述
    • 此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
  • 阶段二(Confirm)
    假如要提交(Confirm),则冻结金额扣减30

    • 确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
      请添加图片描述
    • 此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
  • 阶段二(Canncel)
    如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

    • 需要回滚,那么就要释放冻结金额,恢复可用金额:
      请添加图片描述

我们可以把可用余额理解为所有事务参与者共享的, 而冻结金额是各个事务参与者独享的, 事务参与者只能操作自己事务中冻结的部分(commit或者rollback)

TCC不用加全局锁就实现了事务隔离, 性能上优于AT


4.3.2 Seata的TCC模型

Seata中的TCC模型依然延续之前的事务架构,如图:请添加图片描述


4.3.3 优缺点

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

4.3.4 实现TCC模式

4.3.4.1 思路分析

一、定义冻结表
有多少事务参与者,就要设置多少张冻结表, 例如此处我们要去操作account服务、storage服务.那么我们应该创建account_freeze_tbl、storage_freeze_tbl这两张表, 但是为了演示方便, 我们这边暂时只创建前表

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:用来记录事务状态

二、如何写业务?

  • Try业务
    • 记录冻结金额和事务状态到account_freeze表
    • 扣减account表可用金额
  • Confirm业务
    • 根据xid删除account_freeze表的冻结记录
  • Cancel业务
    • 修改account_freeze表,冻结金额为0,state为2
    • 修改account表,恢复可用金额

三、如何判断是否回滚
cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚

四、如何避免业务悬挂?
try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务


4.3.4.2 声明TCC接口、定义实现类

一、声明接口
我们在对应的事务参与者端, 将Try、Confirm、Cancel方法进行声明(基于注解)

我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

package cn.itcast.account.service;

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;

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

@TwoPhaseBusinessAction 加在哪个方法上, 这个方法就是Try

二、编写实现类

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 获取全局事务id
        String xid = RootContext.getXID();
        // 1.扣减可用余额(原逻辑,不动)
        accountMapper.deduct(userId, money);
        
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setXid(xid);	// 全局事务id
        freeze.setUserId(userId);	// 用户id
        freeze.setFreezeMoney(money);	// 冻结金额
        freeze.setState(AccountFreeze.State.TRY);	// 事务状态 Try
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id
        String xid = ctx.getXid();
        
        // 2.根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        // 1.恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}
public interface AccountMapper extends BaseMapper<Account> {

    @Update("update account_tbl set money = money - ${money} where user_id = #{userId}")
    int deduct(@Param("userId") String userId, @Param("money") int money);

    @Update("update account_tbl set money = money + ${money} where user_id = #{userId}")
    int refund(@Param("userId") String userId, @Param("money") int money);
}

4.3.5 空回滚和业务悬挂

一、空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚
请添加图片描述
执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

二、业务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂

执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 获取全局事务id
        String xid = RootContext.getXID();

		// 判断freeze是否有冻结记录, 如果有, 一定是CANCEL执行过, 拒绝业务
		AccountFreeze oldFreeze = freezeMapper.selectById(xid);
		if (oldFreeze  != null) {
			return;
		}
		
        // 1.扣减可用余额(原逻辑,不动)
        accountMapper.deduct(userId, money);
        
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setXid(xid);	// 全局事务id
        freeze.setUserId(userId);	// 用户id
        freeze.setFreezeMoney(money);	// 冻结金额
        freeze.setState(AccountFreeze.State.TRY);	// 事务状态 Try
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id
        String xid = ctx.getXid();
        
        // 2.根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);
		
		// 空回滚的判断, 判断freeze是否为null, 如果为null证明try没有执行, 需要空回滚
		if (freeze == null) {
			String userId = ctx.getActionContext("userId").toString();
			// 记录回滚记录
			AccountFreeze freeze = new AccountFreeze();
	        freeze.setXid(xid);	// 全局事务id
      	  	freeze.setUserId(userId);	// 用户id
      	  	freeze.setFreezeMoney(0);	// 冻结金额
       	 	freeze.setState(AccountFreeze.State.CANCEL);
       	 	freezeMapper.insert(freeze);
       	 	return true;
		}

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

在这里插入图片描述


4.4 SAGA模式

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值