分布式事务(分布式事务的问题,CAP定理和BASE理论,Seata,Seata的事务模式,Seata高可用)

目录

一、分布式事务的问题

1. 本地事务

2. 分布式事务

3. 分布式事务的问题演示

二、CAP定理和BASE理论【面试】

1. CAP定理

2. BASE理论

3. 分布式事务的解决思路

4. 小结

三、Seata入门与整合

1. Seata简介

2. Seata架构

3. Seata部署

1 下载与安装

2 准备数据库

3 配置

4 启动TC服务

4. 微服务集成Seata

1 添加依赖

2 配置tc地址

5. 小结

四、Seata的事务模式 ​​​​​​​

1. XA模式

2. AT模式【重点】

3. TCC模式【了解】

4. SAGA模式【拓展】

5. 四种模式对比

6. 小结

五、Seata高可用【拓展】

1. Seata高可用架构模型

2. 实现高可用

大总结


一、分布式事务的问题

1. 本地事务

事务概念:即传统的单机事务,是数据库的概念,表示由一个或多个操作组成的一个业务。比如:银行转账

事务作用:组成事务的多个操作单元,在操作数据库时,要成功都成功,要失败都失败

事务特性:ACID

  • 多个数据库操作如果想要属于同一个事务:必须使用同一个数据库连接

  • 如果开启了事务,在数据库底层会对数据加锁:如果一个事务长时间不提交,一定会影响性能

2. 分布式事务

1 介绍

在分布式环境上同样需要事务来保证数据的一致性。而因为跨数据源或跨服务环境所导致的传统事务不可用,形成的新的事务需求,这样的事务叫分布式事务。

传统事务中,要想让多个操作属于同一事务,就需要使用同一个数据库连接Connection对象。但是在分布式环境下,通常是做不到这一点的,必须使用分布式事务。比如:

  • 跨数据源的分布式事务:程序要操作不同服务器上的数据库

  • 跨服务的分布式事务:程序要调用多个服务,每个服务都要操作数据库

  • 综合情况

2 示例场景

电商行业中比较常见的下单付款案例,包括下面几个行为:

  • 创建新订单

  • 扣减商品库存

  • 从用户账户余额扣除金额

要完成上面的操作,需要访问三个不同的微服务和三个不同的数据库,如图:

订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务要解决的问题了

3. 分布式事务的问题演示

1 准备数据库

使用SQLyog执行SQL脚本《seata-demo.sql》,初始化数据库

2 导入演示项目

今天的资料里提供了《seata-demo》项目,把这个项目拷贝到你的工作空间目录里

用idea打开这个项目

3 启动服务

  1. 启动nacos

  2. 启动所有微服务

4 测试下单功能

使用Postman发请求下单

请求路径是:http://localhost:8082/order

请求方式是:POST

表单参数是:

  • userId:user202103032042012

  • commodityCode:100202003032041

  • count:20

  • money:200

最终结果是:因为库存量不够,导致扣减库存失败,但是用户的帐户余额已经扣减了。已经出现了分布式事务问题

二、CAP定理和BASE理论【面试】

分布式事务问题的处理,其实就是在数据的一致性与服务的可用性之间做一个权衡:

  • 如果要保证所有子事务的数据一致性:就要舍弃一些服务的可用性。因为数据库事务会对数据行加锁

  • 如果要保证所有服务的可用性:就要考虑一下数据的一致性如何处理

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

1. CAP定理

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。 Eric Brewer 说,这三个指标不可能同时做到,这个结论就叫做 CAP 定理。

 

1 CAP三指标介绍

1 C一致性

一致性Consitency,即 用户访问分布式系统中的任意节点,得到的数据必须一致(业务上的一致)。这就要求节点之间必须要及时同步数据。

比如:集群中有两个节点,初始数据是一致的;当修改了其中一个节点的数据时,要把数据变更立即同步到另外一个节点,保证所有节点的数据是一致的。

2 A可用性

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

3 P分区容错性

分区Partition:因为网络故障或者其它原因,导致分布式系统中的部分节点与其它节点失联,形成独立分区

分区容错性Partition Tolerance,即 集群出现分区时,整个系统也要持续对外提供服务

2 CAP的矛盾

在分布式系统中,分区容错性(P)是必须要保证的。但C和A两个指标就互相矛盾

以上图为例:

  • 因为网络原因形成了两个分区:node01和node02一个分区;node03一个分区

  • 如果我们要修改node02上的数据:

    • 如果要追求一致性:必须等到node02把数据同步到node01、node03,才返回响应。但因为分区了,等待时间不确定,可能要长时间等待==>追求一致性C,舍弃了可用性A

    • 如果要追求可用性:修改了node02的数据就立即返回响应;不能保证数据同步到了node01和node03

      追求了可用性A,舍弃了一致性C

所以CAP定理中,P必须保证,而C和A相互矛盾,只能保证一个。即:

  • CP模式:舍弃可用性,追求一致性

  • AP模式:舍弃一致性,追求可用性

但是,难道这个AC矛盾就不可调和的吗?并不是,BASE理论就提出了完善和弥补的方案

2. BASE理论

BASE理论,是对CAP定理中的CA矛盾进行权衡之后,提供的一种解决思路。这是指:

  • 采用CP模式,追求一致性,舍弃一定的可用性:

    • BA (Basically Available),基本可用:分布式系统在出现故障时,允许损失部分可用性,要保证核心可用

      响应时间的损失:比如原本要求0.5秒内响应,现在允许5秒内响应

      系统功能的损失:出现某些故障时,核心功能保证可用,部分非核心功能允许不可用

  • 采用AP模式,追求可用性,对一致性采用一些补偿措施

    • S (Soft State),软状态:在一定时间内,允许出现中间状态,比如 数据临时不一致

    • E (Eventually Consistent),最终一致性:虽然无法保证强一致性,但是在软状态之后,最终达到数据一致

3. 分布式事务的解决思路

1 解决思路

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

  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

2 几个概念

  • 分支事务RM:ResourceManager 在整个业务中,每一个子系统的事务,称为一个分支事务。

    对应@Transactional

  • 全局事务TM:TransactionManager 一个完整的业务,需要众多分支事务共同组成一个全局事务。

    对应@GlobalTransactional

  • 事务协调者TC:TransactionCoordinator 用于在整个全局事务里,管理、协调各个分支事务的状态。

    对应Seata软件

4. 小结

CAP定理

CAP分别是:
    C:一致性。表示分布式系统各节点之间的数据或状态 应该保持一致。 需要花费一定的时间进行数据同步
    A:可用性。表示访问分布式系统的任意一个节点,都必须立即响应。如果要求立即响应就不能保证数据已经同步完成
    P:分区容错性。表示当系统出现问题形成多个分区时,应当正常提供服务
CAP定理:
    一个分布式系统最多只能满足CAP里的2项,不可能全部满足
    要么AP:追求可用性,就不能保证一致性
    要么CP:追求一致性,就不能保证可用性

BASE理论:对CAP定理的一种权衡后的处理方案

BA:Basically Available,基本可用
    不要求绝对可用,只要做到基本可用即可。
    追求一致性的时候,允许响应时间略有延迟、允许非核心功能暂不可用
SE:S软状态 + E最终一致
    不要求绝对的一致性,只要达到最终一致即可,允许存在临时的数据不一致状态

三、Seata入门与整合 ​​​​​​​

1. Seata简介

Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。

官网地址:Apache Seata,其中的文档、博客中提供了大量的使用说明、源码分析。

2. Seata架构

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

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

    是Seata本身

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

    负责事务的边界。@GlobalTransactional

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

    分支事务

    

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

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式

  • TCC模式:最终一致的分阶段事务模式,有业务侵入

  • SAGA模式:长事务模式,有业务侵入

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

3. Seata部署

seata-server中分布式事务中充当了TC的角色

    

1 下载与安装

1 下载

下载seata-server包,地址 http://seata.io/zh-cn/blog/download.html

也可以直接使用资料里提供好的程序:《seata-server-1.4.2.zip》  

2 安装

seata-server免安装,直接解压到一个不含中文、空格、特殊字符的目录里即可

2 准备数据库

tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,包括全局事务、分支事务、全局锁等信息,因此要提前创建好这些表:

用Navicat或其它工具连接本机MySQL,执行脚本:《seata-tc-server.sql》

3 配置

1 在nacos里添加配置

注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好

  1. 在nacos中新建配置:

    2. 配置的内容如下:

注意:其中的数据库地址、帐号、密码都要修改成自己的

# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=UTC
store.db.user=root
store.db.password=root
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 seata拉取配置文件并注册服务

修改conf目录下的registry.conf文件,完整配置如下:

registry {
  # tc服务的注册中心类型,使用nacos
  type = "nacos"

  # 将tc服务注册到nacos,要配置nacos的地址等信息。 ""@DEFAULT_GROUP@seata-tc-server@TJ
  nacos {
    # tc服务的应用名称,可以自定义
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "TJ"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # 读取tc配置文件的方式:从配置中心nacos里读取配置。这样的话,如果tc搭建集群,可以通过配置中心共享配置
  type = "nacos"

  # 要从nacos读取配置文件信息,要配置nacos的地址等信息
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}

4 启动TC服务

  1. 先启动nacos

  2. 启动seata:

    进入seata的bin目录,运行其中的seata-server.bat

     3.验证是否启动:

        如果启动成功了,seata-server应该已经注册到nacos注册中心了

        我们打开nacos,看一下有没有seata服务

4. 微服务集成Seata

每个需要分布式事务的微服务,都要按照下面的步骤进行配置。

我们以订单服务order-service为例进行说明;其它微服务也要做相同配置

1 添加依赖

修改pom.xml,添加依赖

<!--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>
        <exclusion>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <!--seata starter 采用1.4.2版本-->
    <version>1.4.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.9</version>
</dependency>

2 配置tc地址

修改application.yaml,配置tc地址。通过注册中心nacos,可以拉取tc服务的地址

seata:
  # 要去注册中心nacos里,拉取tc服务的地址
  registry:
    type: nacos
    # tc服务集群注册到了nacos的""@DEFAULT_GROUP@seata-tc-server@TJ
    # 所以要从nacos中拉取 ""@DEFAULT_GROUP@seata-tc-server@TJ  服务集群
    nacos:
      server-addr: localhost:8848 #nacos地址
      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: TJ

5. 小结

安装部署Seata的步骤:

  1. 执行SQL脚本:创建数据库,这个库给seata使用的,seata用来存储事务信息

  2. 在Nacos里准备配置文件:配置文件给seata使用的,seata要拉取这个配置文件,根据配置信息连接数据库

  3. 解压Seata,修改Seata的配置文件:conf/registry.conf

    • config的配置:从哪拉取配置文件。以下配置的参数是

      使用了type为nacos的配置中心

      从 serverAddr对应配置中心里拉取配置文件

      • serverAddr要配置Nacos的地址,username和password是nacos的帐号密码

      • 要从namespace为""的名称空间里,从group为SEATA_GROUP的组里,拉取名称为 seataServer.properties的配置文件

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

registry配置:要把当前seata服务注册到哪

要使用type为nacos的注册中心,把自己(当前seata服务)上报到注册中心nacos里

  • 自己的应用名称是:使用application配置

  • nacos的地址信息:serverAddr,username,password

  • 把当前seata服务注册到:namespace为""的名称空间、group为DEFAULT_GROUP的组、集群名称为BJ

registry {
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "BJ"
    username = "nacos"
    password = "nacos"
  }
}

     4.启动seata

微服务要整合seata:

  • 所有微服务添加依赖坐标

  • 所有微服务修改配置文件:主要是要设置 从哪才能拉取得到seata的地址

seata:
  registry:
    type: nacos  #要从nacos里拉取seata的信息
    nacos:
      server-addr: localhost:8848
      username: nacos
      password: nacos
      
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-server
  tx-service-group: seata-demo #微服务所属的组名,自定义一个组名
  service:
    vgroup-mapping:
      seata-demo: BJ  #设置微服务组关联的seata集群。seata-demo服务组,要使用Bj集群的seata服务

四、Seata的事务模式 ​​​​​​​

1. XA模式

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

1 两阶段提交

XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交:

  • 一阶段:

    事务协调者通知每个事务参与者执行其本地事务

    本地事务执行后暂不提交,继续持有数据库锁;向事务协调者报告事务的执行状态

  • 二阶段:

    事务协调者基于一阶段的报告来决定下一步操作

    如果一阶段所有事务都成功:则通知所有事务参与者都提交事务

    如果一阶段有事务执行失败:则通知所有事务参与者都回滚事务

如图:

  • 如果所有事务都正常:

如果有事务出现异常:

2 Seata的XA模型

Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型

  • 一阶段-RM工作:

    注册分支事务到TC

    执行分支业务的SQL但不提交

    向TC报告事务执行状态

  • 二阶段-TC工作

    TC检测各分支事务的执行状态

    如果都成功:TC通知所有RM提交事务

    如果有失败:TC通知所有RM回滚事务

  • 二阶段-RM工作

    根据TC的通知指定,提交或回滚事务

Seata的XA基本架构如图:

3 XA模式的优缺点

XA模式的优点:

  • 事务的强一致性,满足ACID原则。

  • 多事务(即多分布式事务之间)之间是完全隔离。多事务并发完全不受影响

  • 常用RDBMS数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点:

  • 性能较差,因为一阶段需要锁定数据库资源,等待二阶段结束才释放,所以性能较差

  • 依赖关系型数据库实现事务。NoSQL参与不进来

4 使用示例

Seata的依赖启动器已经完成了XA模式的自动装配,使用起来非常简单,步骤:

  1. 修改配置文件,开启XA模式

  2. 修改全局事务入口方法,添加注解@GlobalTransactional注解:TM全局事务

    每个分支事务的方法上,添加注解@Transactional注解:RM分支事务

  3. 重启测试

1) 开启XA模式

修改每个参与事务的微服务的配置文件,开启XA模式:

seata:
  data-source-proxy-mode: XA

2) 添加注解@GlobalTransactional

在发起全局事务的入口方法上添加注解@GlobalTransactional

在本例中是OrderServiceImpl中的create方法

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

3) 重启测试

重启所有微服务,再次测试

2. AT模式【重点】

1 Seata的AT模型

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

  • 一阶段-RM工作:

    向TC注册事务分支

    记录undo-log(数据变更之前的快照)

    执行业务SQL并提交

    向TC报告事务状态

  • 二阶段-TC工作:

    基于一阶段的报告来决定下一步操作

    如果所有分支事务都成功:通知所有RM提交事务

    如果任一分支事务失败了:通知所有RM回滚事务

  • 二阶段-RM工作:

    如果收到提交通知指令:删除undo-log

    如果收到回滚通知指令:根据undo-log恢复到更新前的数据

2 AT与XA的区别

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。

  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。

  • XA模式强一致;AT模式最终一致

3 脏写问题

多线程并发访问AT模式的分布式事务时,可能出现脏写问题。如图:

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

5 AT模式的优缺点

AT模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好

  • 利用全局锁实现事务之间的隔离

  • 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致

  • 框架的快照功能会影响性能,但比XA模式要好很多

6 使用示例【掌握】

T模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。

步骤:

  1. 准备数据快照表

  2. 开启AT模式

  3. 重启并测试

1) 准备数据库表

执行脚本《undo_log表.sql》,把undo_log表导入到微服务的库。我们这里是seata-demo

这张表用于存储一阶段的undo日志,二阶段回滚时会使用这些日志进行数据恢复;二阶段提交时则直接清除日志

2) 开启AT模式

修改参与分布式事务的所有微服务的配置文件,将事务模式修改为AT模式

可以不配置,因为Seata默认使用的就是AT模式

seata:
  data-source-proxy-mode: AT

3) 添加注解

在全局事务的入口方法上添加注解@GlobalTransactional

我们这里在OrderServiceImplcreate方法上添加

4) 重启测试

  1. 重启所有微服务

  2. 使用Postman测试

3. TCC模式【了解】

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

  • Try:资源的检测和预留;

  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

  • Cancel:预留资源释放,可以理解为try的反向操作。

1 TCC模式的实现流程

举例说明:减扣余额的业务。

  • 假设帐户A原本余额是100,需要减扣30元。

  • 要提前准备一个位置存储冻结金额,例如:创建一张数据库表,存储冻结的金额

一阶段(Try)

检查余额是否充足。如果余额充足,则扣除余额30元,在冻结金额里增加30元。

此时总金额 = 余额 + 冻结金额,总数仍然是100元不变,分支事务可以直接提交,无需等待其它事务

二阶段(Confirm)

如果TC通知要提交,则冻结金额-30,直接提交; 用户的余额不变

此时总金额 = 余额 + 冻结金额,总数是70

三阶段(Cancel)

如果TC通知要回滚,则释放冻结金额,恢复用户余额,即:冻结金额-30,用户余额+30

此时总金额 = 余额 + 冻结金额,总数是100

 

2 Seata的TCC模式

Seata中的TCC模型依然延续之前的事务架构,如图:

3 优缺点

TCC模式的每个阶段做什么:

  • Try:资源检查和预留

  • Confirm:业务执行和提交

  • Cancel:预留资源的释放

TCC的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能好

  • 基于资源预留实现数据隔离;相比AT模型,无需生成快照,无需使用全局锁,性能更强

  • 不依赖数据库事务,而是依赖补偿操作,可以用于非关系型数据库

TCC的缺点:

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦

  • 软状态,事务是最终一致

  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

4 TCC的几个问题

1 空回滚

当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时就要允许空回滚

2 业务悬挂

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

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

3 幂等性

当TC通知RM提交或回滚时,如果RM明明已经提交或回滚,但是因为某些原因(例如网络拥堵)导致没有给TC返回结果,TC会重复通知RM提交或回滚,直到收到结果为止。

为了避免Try或Confirm业务的重复执行,Try和Confirm需要实现幂等:判断一下事务的状态,如果已经处理过,就直接返回成功,结束即可。

 

5 使用示例

我们使用AT和TCC混合使用的方式进行演示:

  • 用户余额处理适合使用TCC,就使用TCC模式

  • 库存处理也适合使用TCC,但是我们TCC比较麻烦,就不处理了,仍然使用AT

  • 创建订单不适合使用TCC,还使用AT模式

解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel。

步骤:

  1. 定义一张表,用于存储冻结金额和事务状态

  2. 定义Try业务、Confirm业务和Cancel业务,并处理业务悬挂和空回滚问题

1) 创建表存储事务状态和冻结数据

在微服务的数据库(我们这里是seata-demo库)里创建表,如下:

CREATE TABLE `account_freeze_tbl`  (
  `xid` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `freeze_money` INT(11) UNSIGNED NULL DEFAULT 0,
  `state` INT(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = INNODB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

其中:

  • xid:全局事务的id

  • user_id:用户id,即 哪个用户的数据

  • freeze_money:冻结金额

  • state:事务状态

2) 实现Try、Confirm和Cancel业务

Try业务:

  • 先根据xid查询account_freeze_tbl表数据,如果找到了说明Cancel已执行,拒绝执行Try业务

  • 如果找不到:

    • 把冻结金额和事务状态保存到account_freeze_tbl表里

    • 减扣帐户表的余额

Confirm业务:

  • 根据xid,删除记录(冻结金额就删除掉了)

Cancel业务:

  • 根据xid先查询account_freeze_tbl表数据,如果找不到说明try还没有做,需要空回滚

  • 如果找到了:

    • 修改account_freeze_tbl表:冻结金额为0,state为2(cancel)

    • 修改帐户表,恢复余额

1. 声明TCC接口

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解进行声明

修改帐户服务account-service,利用TCC实现余额扣减功能:

  • com.itheima.order.service包里创建接口:

  • 注意在接口上添加@LocalTCC注解

package com.itheima.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);
}

2. 编写实现业务

package com.itheima.account.service.impl;

import com.itheima.account.mapper.AccountFreezeMapper;
import com.itheima.account.mapper.AccountMapper;
import com.itheima.account.pojo.AccountFreeze;
import com.itheima.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

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

        //2. 防止业务悬挂:如果已经Cancel了,就拒绝执行Try
        AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
        if (oldFreeze != null) {
            return;
        }

        //3. 扣减余额
        accountMapper.deduct(userId, money);
        //4. 把冻结金额和事务状态存储起来
        AccountFreeze freeze = new AccountFreeze();
        freeze.setXid(xid);
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        accountFreezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        //获取全局事务id
        String xid = ctx.getXid()();
        //删除冻结信息
        int count = accountFreezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    @Transactional
    public boolean cancel(BusinessActionContext ctx) {
        //1. 获取全局事务id
        String xid = ctx.getXid();
        //2. 空回滚判断
        AccountFreeze freeze = accountFreezeMapper.selectById(xid);
        if (freeze == null) {
            freeze = new AccountFreeze();
            freeze.setXid(xid);
            String userId = ctx.getActionContext("userId").toString();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            accountFreezeMapper.insert(freeze);
            return true;
        }

        //3. 如果已经Cancel过了,就不需要重新Cancel
        if (freeze.getState() == AccountFreeze.State.CANCEL) {
            return true;
        }

        //4. 恢复用户余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());

        //5. 清零冻结金额,修改事务状态
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = accountFreezeMapper.updateById(freeze);
        return count == 1;
    }
}

3. 修改Controller

让Controller调用AccountTCCServicededuct方法

@RestController
@RequestMapping("account")
public class AccountController {

    @Autowired
    // private AccountService accountService;
    private AccountTCCService accountService;

    @PutMapping("/{userId}/{money}")
    public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money) {
        accountService.deduct(userId, money);
        return ResponseEntity.noContent().build();
    }
}

3) 在全局事务入口方法上加@GlobalTransactional

在OrderServiceImpl的create方法上添加注解@GlobalTransactional

@Override
@GlobalTransactional
public Long create(Order order) {
    ......;
}

4) 重启测试

使用Postman重新发请求进行下单,结果会下单失败;查看数据库里,订单、余额、库存数据都没变

4. SAGA模式【拓展】

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

其理论基础是Hector & Kenneth 在1987年发表的论文Sagas

Seata官网对于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html

1 原理说明

在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

Saga也分为两个阶段:

  • 一阶段:直接提交本地事务

  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

2 优缺点

优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高

  • 一阶段直接提交事务,无锁,性能好

  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差

  • 没有锁,没有事务隔离,会有脏写。

    某一个环节给你转账过去了,后边的环节出错要补偿撤消。但是转给你的钱,已经被你花掉了

    做法:

    • 宁可长款,不可短款:商家宁可多收你钱,如果出错最后退给你;也不能先给你钱,出错后找你要

    • 具体实现:扣你钱的操作放到前边,给你加钱的操作放到最后的环节

适用场景:

  • 业务流程长、业务流程多

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

5. 四种模式对比

6. 小结

XA模式

XA模式的用法:
    1. 修改所有微服务的配置文件,设置事务模式为XA。 seata.data-source-proxy-mode=XA
    2. 全局事务入口方法上加 @GlobalTransactional;所有分支事务方法上加@Transactional
XA两阶段提交过程:
    一阶段:执行、汇报但不提交
        开启全局事务,各分支事务注册到TC
        各分支事务执行SQL但不提交
        各分支事务向TC汇报状态
    二阶段:最终的提交或回滚
        全局事务要结束,通知TC做最终决策
            如果所有分支事务都成功,就通知所有分支事务一起提交
            如果任意分支事务失败了,就通知所有分支事务一起回滚
        各分支事务:执行TC的最终决策,提交或回滚
XA模式的优缺点:
    优点:
        强一致性,各个分支事务之间是完全一致的,一起提交,一起回滚
        隔离性好:借助于单机事务实现多全局事务的隔离。多全局事务并发不会受影响
        实现简单:没有代码入侵,只要添加注解就能实现XA模式的事务
    缺点:
        可用性弱:因为一阶段开启事务执行SQL但不提交,数据库底层会对数据长时间加锁
        NoSQL参与不进来:依赖于关系型数据库的事务机制实现

AT模式

AT模式的用法:
    1. 在微服务的库里准备一张undo_log表。给seata用的,我们的代码用不上
    2. 修改所有微服务的配置文件,设置事务模式为AT。 seata.data-source-proxy-mode=AT
    3. 全局事务入口方法上加 @GlobalTransactional;所有分支事务方法上加@Transactional
AT的两阶段提交过程:
    一阶段:
        各分支事务注册到TC
        各分支事务执行SQL提交,并备份数据(数据变更前后的数据)
        各分支事务向TC汇报状态
    二阶段:
        TC根据各分支事务状态做最终的决策,通知给所有分支事务
        各分支事务根据TC的通知:
            如果要提交:直接清除undo备份
            如果要回滚:就拿undo备份的数据进行恢复,然后删除undo备份
AT模式优缺点:
    优点:
        实现简单,无代码入侵
        可用性比XA模式要强:因为一阶段直接提交事务
        事务并发时的隔离性:使用全局锁实现,处理脏写问题
    缺点:
        存在脏读脏写问题,隔离性不如XA强
        只支持RDBMS数据库,依赖于关系型数据库本身的事务备份机制
AT模式脏写问题:
    一个全局事务在一阶段提交之后,到二阶段之前  要防止数据被其他人修改掉:加全局锁
    具体的过程是:
        一阶段:全程持有数据的DB锁
            开启事务:抢DB锁,锁定要修改的数据
            执行SQL并备份:备份的是变更前后的数据。比如 余额之前100,之后90
            加全局锁:对数据加全局锁===> 事务xx对xx数据持有全局锁
            提交事务:释放DB锁
        
        一和二阶段之间:持有数据的全局锁
            在这个阶段,其它全局事务不可能抢到数据的全局锁,不可能对数据进行修改
            但是其它 非Seata事务仍然可以修改数据
            
        二阶段:如果要回滚,全程持有数据的DB锁
            开启事务:抢DB锁,锁定要修改的数据
            释放全局锁
            恢复数据:拿备份的数据进行恢复
                先判断:数据库里当前数据,和自己之前修改后的数据是否相同
                如果不同:说明在我一阶段到二阶段之间,数据被其他人修改了,Seata会报错,等待人工干预
                如果相同:说明在我一阶段一二阶段之间,数据没有被别人修改,直接恢复数据即可
            提交事务:释放DB锁

TCC模式

TCC:
    T:Try,是在一阶段执行的,尝试进行资源的预留预扣(冻结)。我们写,我们调
    C:Confirm,是在二阶段执行,相当于事务的提交方法。我们写,由Seata调用
        直接清除冻结的数据
    C:Cancel,是在二阶段执行,相当于事务的回滚方法。我们写,由Seata调用
        把冻结的数据,加回到原始数据里
TCC模式两阶段提交:
    一阶段:
        各分支事务注册到TC
        各分支事务执行Try方法,直接提交
        向TC上报自己的状态
    二阶段:
        TC根据各分支事务的状态做最终的决策,然后通知给所有的分支事务
        各分支事务根据TC决策:
            如果要提交:就执行Confirm方法
            如果要回滚:就执行Cancel方法
TCC的使用示例:
    1. 准备一张表,用于存储冻结的数据;准备这张表的实体类、Mapper等等
    2. 编写TCC接口:Service层的接口
        类上加@LocalTCC
        类里要编写3个抽象方法:
            Try方法:方法名随意,执行数据的扣除与冻结
                方法上加@TwoPhaseBusinessAction(name="唯一标识",commitMethod="confirm方法",rollbackMethod="cancel方法")
                    告诉Seata,哪个方法是Confirm,哪个方法是Cancel
                方法的形参加@BusinessActionContextParameter(paramName="参数名")
                    告诉Seata,把这个形参的值维护起来。后续在Confirm和Cancel方法里通过BusinessActionContext对象可以获取到
            Confirm方法:方法名随意,相当于提交事务,清除冻结数据
            Cancel方法:方法名随意,相当于回滚事务,把冻结的数据加回到原始数据里
    3. 编写TCC接口的实现类,实现Try、Confirm、Cancel方法
        Try方法:
            要防止业务悬挂:防止Try没成功,Cancel之后Try方法执行造成的业务悬挂
            扣除资源
            冻结资源
        Confirm方法:
            相当于提交事务,直接清除冻结的数据
        Cancel方法:
            相当于回滚事务
            要允许空回滚:如果Try还没有执行成功,就执行Cancel了,要允许空回滚
            要实现幂等性:如果Cancel方法被多次重复调用,不应该产生不良后果。如果已经回滚过,就直接结束
            把冻结的数据进行恢复,设置状态为已回滚
TCC模式的优缺点:
    优点:
        可用性强。不需要全局锁了,并且一阶段直接提交事务
        NoSQL也能参与进来,因为两阶段的代码需要我们自己实现
        数据隔离:通过资源预留进行隔离的
    缺点:
        有代码入侵,实现麻烦
        是最终一致,存在数据的临时不一致状态

五、Seata高可用【拓展】

Seata的TC服务作为分布式事务核心,一定要保证seata集群的高可用性。

1. Seata高可用架构模型

搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可,但集群并不能确保100%安全。例如集群所在机房故障了怎么办?

所以如果可用性要求较高,一般都会做异地多机房容灾。比如一个TC集群在上海,另一个TC集群在杭州:

微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。

2. 实现高可用

参考资料里《Seata高可用\seata的部署和集成.md》中的第三章节

大总结

CAP定理

  • C:一致性。分布式系统的多个节点数据和状态要保持一致。靠数据同步实现

  • A:可用性。访问分布式系统的任意节点,都要立即给出响应,不能阻塞、不能拒绝

  • P:分区容错性。分布式系统出现问题形成多分区,整个系统应该继续正常提供服务

  • CAP矛盾:首先分布式系统必须要满足P分区容错性,A和C矛盾

BASE理论:

  • BA:基本可用。只要做到基本可用即可,允许舍弃一定的可用性,比如响应时间稍有延长,非核心功能允许暂不可用

  • SE:软状态 + 最终一致。允许节点之间存在数据的临时不一致,只要最终一致即可

分布式事务的实现技术:Seata

  • TM:全局事务管理器,在代码里指@GlobalTransactional,决定了事务的边界与开启、结束

  • RM:资源管理器,在代码里指@Transactional,是分支事务

  • TC:事务协调者,指Seata软件,用于协调各分支事务的状态,决定提交还是回滚

Seata的事务模式:

  • XA:强一致、弱可用的

    用法:

    • 修改微服务的配置文件设置为XA模式;

    • 全局事务入口方法加@GlobalTransactional,分支事务方法加@Transactional

    两阶段提交:

    • 一阶段:各分支事务注册到TC,执行语句但不提交,向TC汇报状态

    • 二阶段:

      TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务

      分支事务根据TC的通知,执行提交或回滚

    优缺点:

    • 优点:

      强一致性。所有节点一起提交或一起回滚

      实现简单。无代码入侵

      事务并发时完全隔离。通过数据库本地事务实现隔离

    • 缺点:

      可用性弱。一阶段不提交事务,会对数据长时间加DB锁

      依赖于RDBMS本身的事务机制,所以NoSQL不能参与进来

  • AT:弱一致,可用性较强

    用法:

    • 在微服务的库里创建一张undo-log表。给Seata使用的,用来存储备份数据

    • 修改微服务的配置文件设置为AT模式;

    • 全局事务入口方法加@GlobalTransactional,分支事务方法加@Transactional

    两阶段提交:

    • 一阶段:

      各分支事务注册到TC,执行语句备份数据

      先对数据加全局锁,然后再提交事务

      向TC汇报状态

    • 二阶段:

      TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务

      分支事务根据TC的通知,执行提交或回滚

      • 如果 要提交:就直接清除备份数据

      • 如果 要回滚:就使用备份数据进行恢复

        获取DB锁,释放全局锁

        执行数据恢复

        释放DB锁

    AT脏写问题的处理:使用了全局锁

    优缺点:

    • 优点:

      可用性较强。因为一阶段直接提升事务

      使用了全局锁实现事务并发时的隔离

      实现简单。没有代码入侵

    • 缺点:

      存在数据的临时不一致状态

      使用了数据库的快照机制进行备份,并有全局锁,对性能有一定影响

  • TCC:

    用法:和AT模式混用

    • 创建TCC接口:加@LocalTCC;写3个方法

      Try方法上加@TwoPhaseBusinessAction(name="", commitMethod="",rollbackMethod="")

      Try方法形参加@BusinessActionContextParameter(paramName="参数名")

      Confirm和Cancel方法:添加BusinessActionContext形参

    • 实现接口

      Try方法:防止业务悬挂,扣除资源,冻结资源

      Confirm方法:清除冻结数据

      Cancel:允许空回滚,实现幂等性,把冻结的数据进行恢复

    两阶段提交:

    • 一阶段:

      各分支事务注册到TC,执行Try方法并提交,向TC汇报状态

    • 二阶段:

      TC根据各分支事务的状态,决定提交还是回滚并通知给所有分支事务

      分支事务根据TC的通知,执行提交或回滚

      • 如果要提交:就执行Confirm方法

      • 如果要回滚:就执行Cancel方法

    优缺点:

    • 优点:

      可用性强。一阶段直接提交事务,不需要全局锁

      基于资源预留实现事务并发的隔离

      NoSQL也能参与进来

    • 缺点:

      实现麻烦,需要编写TCC三个方法

      数据临时不一致的状态

  • SAGA:长事务模型

    两阶段提交:

    • 一阶段:直接执行正向业务

    • 二阶段:如果一切正常,就直接结束;如果一阶段出错,就要逆向执行所有业务的补充操作

    优缺点:

    • 优点:

      性能强,完全不做隔离

      适用于长事务,可以让第三方系统和旧系统加入全局事务

    • 缺点:

      有脏写问题

      数据不一致,是最终一致

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值