关于后端:分布式事务Seata原理

42次阅读

共计 10257 个字符,预计需要花费 26 分钟才能阅读完成。

一、Seata 介绍:

1、Seata 简介:

Seata 是一款开源的分布式事务解决方案,致力于提供高性能与简略易用的分布式事务服务,为用户提供了 AT、TCC、SAGA 和 XA 几种不同的事务模式:

  • AT 模式:无侵入式的分布式事务解决方案,适宜不心愿对业务进行革新的场景,但因为须要增加全局事务锁,对影响高并发零碎的性能。该模式次要关注多 DB 拜访的数据一致性,也包含多服务下的多 DB 数据拜访一致性问题
  • TCC 模式:高性能的分布式事务解决方案,实用于对性能要求比拟高的场景。该模式次要关注业务拆分,在依照业务横向扩大资源时,解决服务间调用的一致性问题
  • Saga 模式:长事务的分布式事务解决方案,实用于业务流程长且须要保障事务最终一致性的业务零碎。Saga 模式一阶段就会提交本地事务,无锁,长流程状况下能够保障性能,多用于渠道层、集成层业务零碎,事务参与者能够是其它公司的服务也能够是遗留零碎的服务,并且对于无奈进行革新和提供 TCC 要求的接口,也能够应用 Saga 模式

2、Seata 的外围组件:

在 Seata 中次要有以下三种角色,其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署:

  • 事务协调器(TC):保护全局事务的运行状态,负责协调并驱动全局提交或回滚
  • 事务管理器(TM):事务发起方,管制全局事务的范畴,负责开启一个全局事务,并最终发动全局提交或回滚全局的决定
  • 资源管理器(RM):事务参与方,治理本地事务正在解决的资源,负责向 TC 注册本地事务、汇报本地事务状态,接管 TC 的命令来驱动本地事务的提交或回滚

3、Seata 的整体执行流程:

Seata 分布式事务的整体执行机制如上图所示,能够大抵分为两阶段提交:

  • ① 发起方 TM 向 TC 申请开启一个全局事务,全局事务创立胜利并生成惟一的全局事务标识 XID,该 XID 在后续事务的服务调用链路的上下文流传(通过 Aop 实现 ))
  • ② RM 向 TC 注册分支事务,汇报资源筹备情况,并与 XID 进行绑定(Branch 分支事务指分布式事务中每个独立的本地部分事务)
  • ③ TM 向 TC 发动 XID 下的所有分支事务的全局提交或回滚申请(事务一阶段完结)
  • ④ TC 汇总事务信息,决定分布式事务是提交还是回滚;
  • ⑤ TC 告诉所有 RM 提交 / 回滚 资源,事务二阶段完结;

二、Seata 的 AT 模式原理:

Seata AT 模式是基于 XA 事务(XA 是基于数据库实现的分布式事务协定)演进而来,须要数据库反对,如果是 MySQL,则须要 5.6 以上版本才反对 XA 协定。AT 模式的特点就是对业务无入侵式,用户只须要关注本人的业务 SQL,Seata 框架会在第一阶段拦挡并解析用户的 SQL,并保留其变更前后的数据镜像,造成 undo log,并主动生成事务第二阶段的提交和回滚操作。

1、AT 模式的整体执行流程:

AT 模式 RM 驱动分支事务的行为分为以下两个阶段:

(1)执行阶段:

  • (1)代理 JDBC 数据源,拦挡并解析业务 SQL,生成更新前后的镜像数据,造成 UNDO LOG。
  • (2)向 TC 注册分支。
  • (3)分支注册胜利后,把业务数据的更新和 UNDO LOG 放在同一个本地事务中提交。

(2)实现阶段:

  • 全局提交,收到 TC 的分支提交申请,异步删除相应分支的 UNDO LOG。
  • 全局回滚,收到 TC 的分支回滚申请,查问分支对应的 UNDO LOG 记录,生成弥补回滚的 SQL 语句,执行分支回滚并返回后果给 TC

2、AT 模式两阶段具体流程:

2.1、第一阶段的具体执行流程:

在第一阶段,RM 写表时,Seata 通过代理数据源(从而达到对业务无侵入的成果)拦挡业务 SQL 并 解析 SQL 语义,找到该 SQL 要更新的业务数据保留成 before image(前置镜像),而后执行业务 SQL,在业务数据更新后,再将其保留成 after image(后置镜像),最初生成行锁。通过把更新前后的业务数据数据镜像组织成回滚日志 undo log,并利用本地事务的 ACID 个性,将业务数据的更新和回滚日志 undo log 的写入在同一个本地事务中提交,保障任何提交的业务数据的更新肯定有相应的回滚日志存在(即操作的原子性)。

基于这样的机制,分支的本地事务就能够在全局事务的第一阶段提交,并马上开释本地事务锁定的资源,这也是 Seata 的 AT 模式和 XA 事务的不同之处,两阶段提交往往对资源的锁定须要继续到第二阶段理论的提交或者回滚操作,而有了回滚日志之后,能够在第一阶段开释对资源的锁定,升高了锁范畴,提高效率,即便第二阶段产生异样须要回滚,只需找对 undo log 中对应数据镜像并反解析成 SQL 来达到回滚目标

2.2、第二阶段提交的具体执行流程:

如果二阶段的全局表决后果是提交的话,阐明所有分支事务的业务 SQL 曾经在第一阶段失效,此时 Seata 框架只需异步删除所有分支第一阶段保留的镜像数据、回滚日志和行锁,实现数据清理即可,这个过程是十分疾速的

2.3、第二阶段回滚的具体执行流程:

如果第二阶段是回滚的话,Seata 就须要回滚第一阶段已执行的 SQL 进行还原业务数据。由 TC 告诉所有 RM 进行依据第一阶段的回滚日志 undo log(即 before image)进行反向弥补,RM 收到 TC 发来的回滚申请后,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过 undo log 生成反向的更新 SQL 并执行,以实现分支的业务数据还原,最初删除 undo log、redo log 和行锁。但在还原前须要校验脏写,比照数据库”以后业务数据”和“after image”,如果两份数据完全一致就阐明没有脏写,能够还原业务数据,如果不统一就阐明有脏写,呈现脏写就须要转人工解决。

三、Seata 的 TCC、Saga、XA 模式原理:

文章第二局部介绍了 Seata 的 AT 模式,接下来咱们就介绍下 Seata 的其实几种事务模式:

1、TCC 模式:

TCC 模式 RM 驱动分支事务的行为分为以下两个阶段:

(1)执行阶段:

  • ① 向 TC 注册分支。
  • ② 执行业务定义的 Try 办法。
  • ③ 向 TC 上报 Try 办法执行状况:胜利或失败。

(2)实现阶段:

  • 全局提交,收到 TC 的分支提交申请,执行业务定义的 Confirm 办法。
  • 全局回滚,收到 TC 的分支回滚申请,执行业务定义的 Cancel 办法。

2、Saga 模式:

Saga 模式 RM 驱动分支事务的行为蕴含以下两个阶段:

(1)执行阶段:

  • ① 向 TC 注册分支。
  • ② 执行业务办法。
  • ③ 向 TC 上报业务办法执行状况:胜利或失败。

(2)实现阶段:

  • 全局提交,RM 不须要解决。
  • 全局回滚,收到 TC 的分支回滚申请,执行业务定义的弥补回滚办法。

3、XA 模式:

XA 模式 RM 驱动分支事务的行为蕴含以下两个阶段:

(1)执行阶段:

  • ① 向 TC 注册分支
  • ② XA Start,执行业务 SQL,XA End
  • ③ XA prepare,并向 TC 上报 XA 分支的执行状况:胜利或失败

(2)实现阶段:

  • 收到 TC 的分支提交申请,XA Commit
  • 收到 TC 的分支回滚申请,XA Rollback

参考文章:

https://help.aliyun.com/document\_detail/157850.html

https://blog.csdn.net/k6T9Q8XKs6iIkZPPIFq/article/details/107…

四、SpringCloud Alibaba 整合 Seata AT 模式:

1、搭建 Seata TC 协调者:

1.1、下载 Seata TC 协调者:

Seata 的协调者其实就是阿里开源的一个服务,咱们只须要下载(下载地址:https://github.com/seata/seata/releases)并启动它,下载实现后,间接解压即可,然而此时还不能间接运行,还须要做一些配置

1.2、创立 TC 所须要的表:

https://github.com/seata/seata/tree/1.4.2/script/server/db

TC 运行须要将事务信息保留在数据库,因而须要创立一些表,找到 seata-1.4.2 源码的 script\server\db 这个目录,将会看到以下 SQL 文件:

以 MySQL 数据库为例,创立数据库 seata,并执行 mysql.sql 文件中的 sql 语句:

CREATE TABLE IF NOT EXISTS `global_table`
(`xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

创立的三张表如下图:

  • global\_table:全局事务表,每当有一个全局事务发动后,就会在该表中记录全局事务的 ID
  • branch\_table:分支事务表,记录每一个分支事务的 ID,分支事务操作的哪个数据库等信息
  • lock\_table:全局锁

1.3、批改 TC 的注册核心和配置核心:

找到 seata-server-1.4.2\seata\conf 目录,其中有一个 registry.conf 文件,其中配置了 TC 的注册核心和配置核心。默认的注册核心是 file 模式,理论应用中必定不能应用,须要改成 Nacos 模式;同样 Seate 的 TC 的配置核心默认也是应用 file 模式,须要批改为 nacos 作为配置核心:

registry {
  # file、nacos、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    namespace = "XXXXXXXXXX"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # file、nacos、apollo、zk、consul、etcd3
  type = "nacos"
  nacos {
    serverAddr = "localhost:8848"
    namespace = "XXXXXXXXXX"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}

须要改变的中央如下:

  • type:改成 nacos,示意应用 nacos 作为注册核心或者配置核心
  • application:服务的名称
  • serverAddr:nacos 的地址
  • group:分组
  • namespace:命名空间
  • username:用户名
  • password:明码

上述配置批改好之后,在 TC 启动的时候将会主动读取 nacos 的配置,最初这份 registry.conf 文件的配置须要与下文每个 seata 客户端我的项目中的配置统一

1.4、导入 seata Server 配置:

倡议将 seata 我的项目下载到本地并浏览 https://github.com/seata/seata/tree/1.4.2/script/config-center 门路下的 README.md 内容

在下面咱们曾经配置好 seata TC 的配置核心,那么 TC 须要存储到 Nacos 的配置都有哪些,如何推送过来呢?在 https://github.com/seata/seata/tree/1.4.2/script/config-center 中有一个 confIg.txt 文件,其中就是 TC 须要的全副配置,而 https://github.com/seata/seata/tree/1.4.2/script/config-center/nacos 中有一个 nacos-config.sh 脚本可能将 config.txt 中的全副配置主动推送到 nacos 中。

咱们先启动 nacos 服务,而后在 nacos-config.sh 目录下执行上面命令,将 config.txt 中的配置信息推送到 nacos 中:

\# -h 代表 nacos 服务的 IP;-p 代表 nacos 的端口号;-g 分组信息;-t 命名空间 ID;-u 用户名,-p 明码

$ sh nacos-config.sh -h 127.0.0.1 -p 8080 -g SEATA\_GROUP -t 7a7581ef-433d-46f3-93f9-5fdc18239c65 -u nacos -w nacos

命令执行胜利后,就能够在 nacos 中看到如下配置:

1.5、批改 TC 的事务信息存储形式:

seata 的 TC 端的事务信息存储模式(store.mode)现有 file、db、redis 三种,file 模式无需改变,间接启动即可,上面专门讲下 db 和 redis 启动步骤。

注:file 模式为单机模式,全局事务会话信息内存中读写并长久化本地文件 root.data,性能较高;

(1)以 DB 模式存储:

db 模式为高可用模式,全局事务会话信息通过 db 共享,相应性能差些;上一节的内容曾经将所有的配置信息都推送到了 Nacos 中,TC 启动时会从 Nacos 中读取,因而咱们批改也须要在 Nacos 中批改。须要批改的配置如下:

## 采纳 db 的存储模式
store.mode=db
## druid 数据源
store.db.datasource=druid
## mysql 数据库
store.db.dbType=mysql
## mysql 驱动
store.db.driverClassName=com.mysql.jdbc.Driver
## TC 的数据库 url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用户名
store.db.user=root
## 明码
store.db.password=Nov2014

在 nacos 中搜寻上述的配置,间接批改其中的值,比方批改 store.mode,如下图:

(2)以 Redis 模式存储:

redis 模式 Seata-Server 1.3 及以上版本反对,性能较高,但存在事务信息失落危险,所以须要提前配置适合以后场景的 redis 长久化配置,该模式需改变以下配置:

store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456

1.6、启动 TC:

依照上述步骤全副配置胜利后,则能够启动 TC,在 seata-server-1.4.2\bin 目录下间接点击 seata-server.bat(windows)运行,启动胜利后,启动胜利后,在 Nacos 的服务列表中则能够看到 TC 曾经注册进入,如下图:

至此,Seata 的 TC 就启动实现了 …………

2、搭建 Seata 客户端(RM):

2.1、maven 增加 seata 依赖:

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
   <!-- 排除依赖,指定版本和服务端统一 -->
    <exclusions>
       <exclusion>
           <groupId>io.seata</groupId>
           <artifactId>seata-spring-boot-starter</artifactId>
       </exclusion>
       <exclusion>
           <groupId>io.seata</groupId>
           <artifactId>seata-all</artifactId>
       </exclusion>
   </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>
<dependency>
   <groupId>io.seata</groupId>
   <artifactId>seata-all</artifactId>
   <version>1.4.2</version>
</dependency>

留神:seata 客户端的依赖版本必须要和服务端统一。

2.2、application.yml 增加 seate 配置:

seata:
  # 这里要特地留神和 nacos 中配置的要保持一致,倡议配置成 ${spring.application.name}-tx-group
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      # 配置所在命名空间 ID,如未配置默认 public 空间
      server-addr: 127.0.0.1:8848
      namespace: XXXXXXXXXX
      group: SEATA_GROUP
      application: seata-server
      userName: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: XXXXXXXXXX
      group: SEATA_GROUP
      userName: nacos
      password: nacos

tx-service-group 配置的值能够自定义,然而定义后须要在 nacos 配置核心新增 service.vgroupMapping.xxx=default 的配置,该属性肯定肯定要和 seata 服务端的配置统一,否则不失效;比方上述配置中的,就须要在 nacos 配置核心新增一个配置项 service.vgroupMapping.my\_test-tx-group=default,并且设置分组为 SEATA\_GROUP,如下图:

留神:my\_test-tx-group 仅仅是后缀,在 nacos 中增加配置时记得要加上前缀 service.vgroupMapping.

2.3、数据库中增加回滚日志表:

https://github.com/seata/seata/tree/1.4.2/script/client/at/db

回滚日志表:undo\_log,这是 Seata 要求必须有的,每个业务库都应该创立一个,SQL 如下:

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

2.4、应用 seata 作为全局事务管制:

在分布式业务入口减少全局事务注解 @GlobalTransactional,其余 service 接口无需配置;假如 A 服务调用 B 服务,那么就在 A 服务的办法下面退出,B 服务下面不必加

/**
     * seata 的 GlobalTransactional 注解只须要加载分布式业务入口处,其余 service 接口无需配置,从而实现分布式事务
     */
    @GlobalTransactional
    @PostMapping (value = "addOrder",produces = {"application/json;charset=utf-8"})
    public String addOrder(@RequestParam String title, @RequestParam int price, @RequestParam int shopId, @RequestParam int userId)
    {shopOrderService.save(ShopOrderPojo.builder().price(price).title(title).shopId(shopId).userId(userId).build());
        storageFeignService.reduceStorage(shopId, 1);
        return "success";
    }

备注:如果你进行异样捕获,seata 将认为你已进行异样解决,就不会回滚数据了

  • (1)比方如果你配置了 @ControllerAdvice 将可能导致数据不回滚
  • (2)如果应用 Feign 调用分布式服务并配置了 fallback,前面服务抛出异样会间接执行 fallback 导致无奈回滚 (rollbackFor = Exception.class);这时能够在 fallback 的实现办法内手动调用 seata 全局回滚,如下所示:
    @Override
        public BizResponse insertAge(Integer age) {
            //feign 调用接口 fallback 后须要手动调用全局事务回滚
            try {String xid = RootContext.getXID();
                GlobalTransactionContext.reload(xid).rollback();} catch (TransactionException e) {e.printStackTrace();
            }
            return BizResponse.fail("客户端降级解决 insertAge," + Thread.currentThread().getName());
}

2.5、undo log 表介绍:

下面介绍过 Seata 是依据 undo\_log 中的记录来回滚的,然而异样回滚后 undo\_log 表却为空?怎么回事,这是因为 undo\_log 日志被删除了,想要看到 undo\_log 表中记录,咱们须要打断点来看,在异样还没抛出时打断点,而后看下数据库 undo\_log 表中数据状况:

undo log 表简略介绍:https://blog.csdn.net/hosaos/article/details/89136666

正文完
 0