• SpringCloud Alibaba——分布式事务 Seata


    一、分布式事务问题

    1.1 本地事务

    本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则,即ACID:

    ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

    原子性:事务中的所有操作,要么全部成功,要么全部失败。
    一致性:要保证数据库内部完整性约束、声明性约束。
    隔离性:对同一资源操作的事务不能同时发生。
    持久性:对数据库做的一切修改将永久保存,不管是否出现故障。

    1.2 分布式事务

    分布式事务,就是指不是在单个服务或单个数据库架构下产生的事务,例如:

    • 跨数据源的分布式事务
    • 跨服务的分布式事务
    • 综合情况

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

    • 创建新订单
    • 扣减商品库存
    • 从用户账户余额扣除金额

    完成上面的操作需要访问三个不同的微服务和三个不同的数据库。
    在这里插入图片描述
    订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证 ACID 原则。

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

    此时 ACID 难以满足,这是分布式事务要解决的问题。

    1.3 演示分布式事务问题

    代码下载地址:https://gitee.com/taurusGitee/springcloud-seata

    1)创建 seata_demo 数据库,并执行下面的 SQL 语句。

    
    create database seata_demo;
    
    use seata_demo;
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    
    DROP TABLE IF EXISTS `account_tbl`;
    CREATE TABLE `account_tbl`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `money` int(11) UNSIGNED NULL DEFAULT 0,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
    
    
    INSERT INTO `account_tbl` VALUES (1, 'user202103032042012', 1000);
    
    
    DROP TABLE IF EXISTS `order_tbl`;
    CREATE TABLE `order_tbl`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `count` int(11) NULL DEFAULT 0,
      `money` int(11) NULL DEFAULT 0,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
    
    
    DROP TABLE IF EXISTS `storage_tbl`;
    CREATE TABLE `storage_tbl`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `count` int(11) UNSIGNED NULL DEFAULT 0,
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE INDEX `commodity_code`(`commodity_code`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
    
    
    INSERT INTO `storage_tbl` VALUES (1, '100202003032041', 10);
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    2)将代码导入到 IDEA 中
    在这里插入图片描述
    其中:

    seata-demo:父工程,负责管理项目依赖

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

    3)启动 Nacos,启动所有的微服务

    4)测试下单功能,发出Post请求:

    请求如下:

    curl --location --request POST 'http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=20&money=200'
    
    • 1

    如图:
    在这里插入图片描述
    此时订单创建成功,账户及库存扣减成功。
    订单表:
    在这里插入图片描述
    账户表:
    原 money 数为 1000,扣减 200 后为 800。
    在这里插入图片描述
    原库存数为 10,扣减 2 后为 8。
    在这里插入图片描述

    修改订单数,将 count 改为 10,超出现有库存数 8,重新测试:
    在这里插入图片描述
    请求没有成功。

    在这里插入图片描述
    在这里插入图片描述
    但是,经过测试发现,当库存不足时,如果余额已经扣减,并不会回滚,出现了分布式事务问题。
    在这里插入图片描述
    首先,每一个服务都是独立的,库存服务抛出异常,账户服务并不知道。另外,由于每一个服务都是独立的,所以它们的事务也都是独立的,订单服务和账户服务在执行完业务之后,事务完成提交,无法回滚。所以,当库存服务抛出异常时,就无法达成事务的一致性。

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

    二、理论基础

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

    2.1 CAP 定理

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

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

    在这里插入图片描述
    它们的第一个字母分别是 C、A、P。

    Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

    2.1.1 一致性

    Consistency(一致性):一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。
    比如,现在包含两个节点,假设 node01 是主节点,node02 是从节点,其中的初始数据是一致的:
    在这里插入图片描述
    当我们修改其中一个节点的数据时,两者的数据产生了差异:
    在这里插入图片描述
    要想保持一致性,就必须实现 node01 到 node02 的数据同步:
    在这里插入图片描述
    如何实现一致性?
    1、写入主数据库后要将数据同步到从数据库。
    2、写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。

    分布式系统一致性的特点:
    1、由于存在数据同步的过程,写操作的响应会有一定的延迟。
    2、为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
    3、如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据(为了保证数据一致性,从任意结点读取到的数据都必须是最新的状态)。

    2.1.2 可用性:

    Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
    如图,有三个节点的集群,访问任何一个都可以及时得到响应:
    在这里插入图片描述
    当有部分节点因为网络故障或其他原因无法访问时,代表节点不可用:
    在这里插入图片描述
    数据同步会导致被同步的数据被锁定,而无法查询。

    如何实现可用性?
    1、写入主数据库后要将数据同步到从数据库。
    2、由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
    3、即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。

    分布式系统可用性的特点:
    1、 所有请求都有响应,且不会出现响应超时或响应错误。

    2.1.3 分区容错性

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

    如何实现分区容忍性?
    1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
    2、添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。

    分布式分区容忍性的特点:
    1、分区容忍性分是布式系统具备的基本能力

    2.1.4 矛盾

    在分布式系统中,系统间的网络不能 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):
    在这里插入图片描述

    这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。

    三、初识 Seata

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

    官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。

    3.1 Seata的架构

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

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

    整体的架构如图:
    在这里插入图片描述
    Seata 基于上述架构提供了四种不同的分布式解决方案:

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

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

    3.2 部署 TC 服务

    3.2.1 下载

    首先我们要下载seata-server包,地址在https://seata.io/zh-cn/blog/download.html
    在这里插入图片描述

    3.2.2.解压

    在非中文目录解压缩这个zip包,其目录结构如下:
    在这里插入图片描述

    3.2.3.修改配置

    修改conf目录下的 application.yml 文件:
    在这里插入图片描述
    具体配置如下:

    server:
      port: 7091
    
    spring:
      application:
        name: seata-server
    
    logging:
      config: classpath:logback-spring.xml
      file:
        path: ${user.home}/logs/seata
      extend:
        logstash-appender:
          destination: 127.0.0.1:4560
        kafka-appender:
          bootstrap-servers: 127.0.0.1:9092
          topic: logback_to_logstash
    
    console:
      user:
        username: seata
        password: seata
    
    seata:
      config:
        # support: nacos, consul, apollo, zk, etcd3
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace:
          group: SEATA_GROUP
          username: nacos
          password: nacos
          ##if use MSE Nacos with auth, mutex with username/password attribute
          #access-key: ""
          #secret-key: ""
          data-id: seataServer.properties
      registry:
        # support: nacos, eureka, redis, zk, consul, etcd3, sofa
        type: nacos
        nacos:
          application: seata-tc-server
          server-addr: 127.0.0.1:8848
          group: DEFAULT_GROUP
          namespace:
          cluster: SH
          username: nacos
          password: nacos
          ##if use MSE Nacos with auth, mutex with username/password attribute
          #access-key: ""
          #secret-key: ""
      store:
        # support: file 、 db 、 redis
        mode: db
        db:
          datasource: druid
          db-type: mysql
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
          user: mysql
          password: 123
          min-conn: 5
          max-conn: 100
          global-table: global_table
          branch-table: branch_table
          lock-table: lock_table
          distributed-lock-table: distributed_lock
          query-limit: 100
          max-wait: 5000
    #  server:
    #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
      security:
        secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
        tokenValidityInMilliseconds: 1800000
        ignore:
          urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    3.2.4.在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=mysql
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    其中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。

    如果此处自己不想手动添加配置,可以打开解压的下面的 config.txt 文件,修改其中的数据库配置。
    在这里插入图片描述
    在这里插入图片描述
    然后执行 nacos 目录下的 nacos-config.sh,执行结束后,nacos 中会自动添加好相应的配置。

    3.2.5.创建数据库表

    特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。

    新建一个名为seata的数据库,运行/seata/script/server/db 目录下的 mysql.sql 文件:
    在这里插入图片描述
    这些表主要记录全局事务、分支事务、全局锁信息:

    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    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_status_gmt_modified` (`status` , `gmt_modified`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    -- 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 = utf8mb4;
    
    -- 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),
        `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_status` (`status`),
        KEY `idx_branch_id` (`branch_id`),
        KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `distributed_lock`
    (
        `lock_key`       CHAR(20) NOT NULL,
        `lock_value`     VARCHAR(20) NOT NULL,
        `expire`         BIGINT,
        primary key (`lock_key`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    3.2.6.启动TC服务

    进入bin目录,运行其中的 seata-server.sh 即可:
    在这里插入图片描述
    启动成功后,seata-server应该已经注册到nacos注册中心了。

    打开浏览器,访问nacos地址:http://localhost:8848,然后进入服务列表页面,可以看到seata-tc-server的信息:
    在这里插入图片描述

    3.3 微服务集成seata

    3.3.1.引入依赖

    首先,我们需要在微服务中引入seata依赖:

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        <exclusions>
            
            <exclusion>
                <artifactId>seata-spring-boot-starterartifactId>
                <groupId>io.seatagroupId>
            exclusion>
        exclusions>
    dependency>
    
    <dependency>
        <groupId>io.seatagroupId>
        <artifactId>seata-spring-boot-starterartifactId>
        <version>${seata.version}version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.3.2.修改配置文件

    需要修改application.yml文件,添加一些配置:

    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中的服务名称
          cluster: SH
      tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
      service:
        vgroup-mapping: # 事务组与TC服务cluster的映射关系
          seata-demo: SH
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    nacos 服务名称组成包括:namespace + group + serviceName + cluster
    seata 客户端获取 tc 的 cluster 的名称方式:以 tx-group-service 的值为 key 到 vgroupMapping 中查找

    注意 order-service、storage-service、account-service 都需要修改。
    修改完成后,将服务启动。

    四、动手实践

    4.1 XA 模式

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

    4.1.1.两阶段提交

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

    什么是 2PC?
    2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

    整个事务过程由事务管理器和参与者组成,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。

    在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议,如下图:

    1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
    2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

    正常情况:
    在这里插入图片描述
    异常情况:
    在这里插入图片描述
    一阶段:

    • 事务协调者通知每个事务参与者执行本地事务
    • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

    二阶段:

    • 事务协调者基于一阶段的报告来判断下一步操作
    • 如果一阶段都成功,则通知所有事务参与者,提交事务
    • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

    4.1.2.Seata的XA模型

    Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
    在这里插入图片描述
    RM(资源管理器) 一阶段的工作:
    ① 注册分支事务到 TC(事务协调者)
    ② 执行分支业务 SQL 但不提交
    ③ 报告执行状态到 TC

    TC 二阶段的工作:

    • TC 检测各分支事务执行状态
      a.如果都成功,通知所有 RM 提交事务
      b.如果有失败,通知所有 RM 回滚事务
      RM 二阶段的工作:
    • 接收 TC 指令,提交或回滚事务

    4.1.3 优缺点

    XA 模式的优点:

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

    XA 模式的缺点:

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

    4.1.4 实现 XA 模式

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

    seata:
      data-source-proxy-mode: XA
    
    • 1
    • 2

    2)给发起全局事务的入口方法添加 @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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    在这里插入图片描述

    账户表:
    在这里插入图片描述
    订单表:
    在这里插入图片描述

    库存表:
    在这里插入图片描述

    4.2.AT模式

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

    4.2.1.Seata的AT模型

    基本流程图:
    在这里插入图片描述
    阶段一 RM 的工作:

    • 注册分支事务
    • 记录 undo-log(数据快照)
    • 执行业务 SQL 并提交
    • 报告事务状态

    阶段二提交时 RM 的工作:

    • 删除 undo-log 即可

    阶段二回滚时 RM 的工作:

    • 根据 undo-log 恢复数据到更新前

    4.2.2 流程梳理

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

    idmoney
    1100

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

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

    AT 模式下,当前分支事务执行流程如下:
    一阶段:
    1)TM 发起并注册全局事务到 TC
    2)TM 调用分支事务
    3)分支事务准备执行业务 SQL
    4)RM 拦截业务 SQL,根据 where 条件查询原始数据,形成快照。

    {
    	"id": 1, "money": 100
    }
    
    • 1
    • 2
    • 3

    5)RM 执行业务 SQL,提交本地事务,释放数据库锁,此时 money=90
    6)RM 报告本地事务状态给 TC

    二阶段:
    1)TM 通知 TC 事务结束
    2)TC 检查分支事务状态

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

    流程图:
    在这里插入图片描述

    4.2.3 AT 与 XA 的区别

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

    4.2.4.脏写问题

    在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
    在这里插入图片描述
    假设线程 A 开启了事务 1,事务开启后,首先 RM 会拦截业务 SQL 的执行,获取数据库锁,接着形成快照 {"id": 1, "money": 100}。RM在拿到快照之后,事务 1 便开始执行业务 SQLset money=90 ,紧接着提交本地事务,并释放数据库锁。至此,一阶段执行结束,二阶段准备开始执行。而恰巧此时,线程 B 开启了另一个事务 2,来执行该业务。首先,事务 2 需要先获取数据库锁,但由于事务 1 拿着数据库锁,只能等待事务 1 释放锁。当事务 1 释放锁后,事务 2 拿到锁,接着 RM 就会去形成快照{"id": 1, "money": 90}。快照形成后,事务 2 便开始执行业务 SQLset money=80 ,紧接着提交本地事务,并释放数据库锁。此时事务 1 正在等待事务 2 释放数据库锁。在事务 2 释放数据库锁且事务 1 拿到锁后,事务 1 便会立即执行二阶段回滚操作,RM 便会根据一阶段形成的快照来恢复数据,但是事务1的快照是 {"id": 1, "money": 100},而在事务 2 执行结束后,money 的值已经变成了 80,通过事务 1 的快照恢复,就会直接将 80 覆盖为 100。

    解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
    所谓全局锁,就是由 TC 记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。那么 TC 是如何记录当前正在操作某行数据的事务?会有一张数据库表,表中会记录当前执行事务的 id,操作哪张表 table,以及哪一行数据(pk 数据的主键值)
    在这里插入图片描述
    假设线程 A 开启了事务 1,事务开启后,首先 RM 会拦截业务 SQL 的执行,获取数据库锁,接着形成快照 {"id": 1, "money": 100}。RM在拿到快照之后,事务 1 便开始执行业务 SQLset money=90 ,业务 SQL 执行结束但尚未提交事务之前,事务 1 会去获取全局锁,获取成功后,会将当前正在操作的数据的事务信息记录下来,然后 RM 便可以放心大胆地去提交本地事务,并释放数据库锁。至此,一阶段执行结束,二阶段准备开始执行。而恰巧此时,线程 B 开启了另一个事务 2,来执行该业务。首先,事务 2 需要先获取数据库锁,但由于事务 1 拿着数据库锁,只能等待事务 1 释放锁。当事务 1 释放锁后,事务 2 拿到锁,接着 RM 就会去形成快照{"id": 1, "money": 90}。快照形成后,事务 2 便开始执行业务 SQLset money=80 ,紧接着事务 2 也尝试获取全局锁,但是由于此时事务 1 持有着全局锁,导致事务 2 不断去重复获取全局锁,直至事务 1 释放全局锁。而此时事务 1 开始执行二阶段回滚操作,需要先获取数据库锁,但是此时数据库锁被事务 2 所持有,导致事务 1 也在等待事务 2 释放数据库锁。由于事务 1 和事务 2 彼此之间相互等待对方释放锁,也就产生了死锁。为了解决这种情况,Seata 设置了重试获取全局锁的次数,默认为 30 次,间隔 10 毫秒。而由于获取 DB 锁的等待时间往往比较长,所以事务 2 最终会由于超时而回滚,并释放数据库锁。接着事务 1 便会获取到数据库锁,根据快照恢复数据。

    全局锁是由 Seata 来进行管理的,它与数据库锁是有区别的。假设有两个事务来同时更新 money 值,此时事务 2 为非 Seata 管理的全局事务,这样就会重新产生脏写的问题。

    在这里插入图片描述
    假设线程 A 开启了事务 1,事务开启后,首先 RM 会拦截业务 SQL 的执行,获取数据库锁,接着形成快照 {"id": 1, "money": 100}。RM在拿到快照之后,事务 1 便开始执行业务 SQLset money=90 ,业务 SQL 执行结束但尚未提交事务之前,事务 1 会去获取全局锁,获取成功后,会将当前正在操作的数据的事务信息记录下来,然后 RM 便可以放心大胆地去提交本地事务,并释放数据库锁。至此,一阶段执行结束,二阶段准备开始执行。而恰巧此时,线程 B 开启了另一个事务 2,该事务并不受 Seata 管理,是一个普通事务,同样也是执行该业务。由于事务 1 释放了数据库锁,使得事务 2 获取到了数据库锁,事务 2 执行业务 SQL set money=80,然后提交事务,释放数据库锁。在事务 2 执行的过程中,事务 1 一直在等待事务 2 释放数据库锁。事务 1 在重新获取到数据库锁后,执行二阶段回滚事务。当事务 1 准备恢复数据时,除了会获取更新前的快照(用于做数据恢复),还会获取更新后的快照,获取更新后的快照是为了与当前数据库中的数据进行对比,如果一样,证明在一阶段与二阶段之间,没有其他事务修改过数据,如果不一样,则说明数据发生过修改。这个时候,Seata 就会停止数据恢复,记录下异常,发送警告,进行人工介入。

    4.2.5 AT 模式优缺点

    AT 模式的优点

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

    AT 模式的缺点:

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

    4.2.6 实现 AT 模式

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

    1)导入数据库表,记录全局锁
    将 lock_table 导入到 TC 服务关联的数据库,undo_log 表导入到微服务关联的数据库。

    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;
    
    
    
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述
    2)修改 application.yml 文件,将事务模式修改为 AT 模式即可:

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

    3)重启服务,并测试
    Account_service 的后台日志:
    在这里插入图片描述
    二阶段执行时,先是rm handle branch roollback,进行了数据回滚,接着删除 undo_log。

    如果向查看 undo_log 中的数据,可以在程序执行过程中打上断点。

    4.3 TCC 模式

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

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

    4.3.1 流程分析

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

    • 阶段一(Try):检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30
      初始余额:
      在这里插入图片描述
      余额充足,可以冻结:
      在这里插入图片描述
      此时,总金额 = 冻结金额 + 可用余额,数量依然是 100 不变。事务直接提交无需等待其他事务。

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

    • 阶段二(Cancel):如果要回滚(Cancel),则冻结金额扣减 30,可用余额增加 30
      需要回滚,那么就要释放冻结金额,恢复可用余额。
      在这里插入图片描述
      TCC 模式在一阶段完成资源预留以后,在二阶段,不管是 Confirm 还是 Cancel,都是操作的自己预留的这部分资源。而正是因为这种特性,导致了 TCC 与 AT 模式有很大的区别。我们来对比一下两者的隔离性和一致性。从一致性上来看,在第一阶段,两种模式都是分支事务各自提交各自的事务,在事务提交完成后,就会释放掉数据库锁,性能上都非常好。但是也正是由于这个原因,一阶段也有可能会执行失败,这时就会出现状态不一致的情况,只有在二阶段完成了回滚或者提交后,才能保证数据的最终一致性。从隔离性上来看,AT 模式需要获取全局锁来实现隔离,保证一二阶段在执行过程中其他事务无法操作。但是在 TCC 模式下,是不需要隔离的,因为在一阶段执行时冻结了一部分余额,假设同时有另一个事务要冻结一部分余额,而该事务要冻结的部分与之前已经冻结的部分是不相关的。而在二阶段中,如果要恢复数据,只需要恢复各自冻结的部分即可。事务之间没有影响,因此也就不需要加锁。TCC 通过资源预留的方式,在二阶段,每个事务各自操作各自预留的资源,互不影响,不需要加锁,自然也就实现了隔离效果。所以,TCC 的性能要优于 AT模式。

    4.3.2 Seata 的 TCC 模型

    Seata 中的 TCC 模型依然延续之前的事务架构,如图:
    在这里插入图片描述
    (1)TM 发起并注册全局事务到 TC
    (2)TM 调用分支事务
    (3)RM 拦截分支事务,并将分支事务注册到 TC
    (4)注册成功后,RM 开始执行分支事务,进行资源预留(Try),资源预留会直接提交事务。
    (5)事务提交完成后,RM 会将事务的状态报告给 TC(一阶段结束)
    (6)TM 通知 TC 事务执行结束,TC 会进行事务状态的判断,检查各分支事务资源是否足够
    (7)如果足够,则执行 Confirm,进行提交;如果不够,则执行 Cancel,进行回滚。

    4.3.3 优缺点

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

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

    TCC 的优点是什么?

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

    TCC 的缺点是什么?

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

    4.3.4 事务悬挂和空回滚

    案例:改造 account-service 服务,利用 TCC 实现分布式事务。
    需求如下:

    • 修改 account-service,编写 try、Confirm、Cancel 逻辑
    • try 业务:添加冻结金额,扣减可用金额
    • Confirm 业务:删除冻结金额
    • Cancel 业务:删除冻结金额,恢复可用金额
    • 保证 Confirm、Cancel 接口的幂等性
    • 允许空回滚
    • 拒绝业务悬挂
    1)幂等性

    幂等性就是无论接口调用多少次,最终达成的效果应该是一样的,不会因为重复调用而出现问题。

    2)空回滚

    当某分支事务的 try 阶段阻塞时,可能导致全局事务超时而触发二阶段的 Cancel 操作。在未执行 try 操作时限执行了 Cancel 操作,这是 Cancel 不能做回滚,就是空回滚。由于在一阶段并没有做资源预留,所以没办法进行回滚,同时,也不能报错,如果报错,seata 会认为 Cancel 出现问题,会进行重试。
    在这里插入图片描述

    执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

    3)业务悬挂

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

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

    4)业务分析

    为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务 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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    其中:

    • 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 业务。

    接下来,我们改造 account-service,利用 TCC 实现余额扣减功能。

    5)声明 TCC 接口

    1)准备工作
    创建 account_freeze_tbl 表,用于记录用户的冻结金额。

    DROP TABLE IF EXISTS `account_freeze_tbl`;
    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;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2)声明TCC接口

    TCC 的 Try、Confirm、Cancel 方法都需要在接口中基于注解来声明,语法如下:
    在这里插入图片描述
    我们在account-service项目中的cn.itcast.account.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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3)编写实现类

    @Service
    public class AccountTccServiceImpl implements AccountTCCService {
    
        @Autowired
        private AccountMapper accountMapper;
        @Autowired
        private AccountFreezeMapper accountFreezeMapper;
    
        @Override
        @Transactional
        public void deduct(String userId, int money) {
            // 获取全局事务 id
            String xid = RootContext.getXID();
            // 业务悬挂处理:判断 freeze 表中是否冻结记录,如果有,一定是 Cancel 执行过,拒绝执行try业务
            AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
            if(oldFreeze != null){
                return;
            }
            // 扣减可用金额
            accountMapper.deduct(userId, money);
            // 记录冻结金额
            AccountFreeze accountFreeze = new AccountFreeze();
            accountFreeze.setUserId(userId);
            accountFreeze.setFreezeMoney(money);
            accountFreeze.setState(AccountFreeze.State.TRY);
            accountFreeze.setXid(xid);
            accountFreezeMapper.insert(accountFreeze);
        }
    
        @Override
        public boolean confirm(BusinessActionContext context) {
            // 获取事务id
            String xid = context.getXid();
            // 根据id删除冻结记录
            int i = accountFreezeMapper.deleteById(xid);
            return i == 1;
        }
    
        @Override
        public boolean cancel(BusinessActionContext context) {
            // 查询冻结记录
            String xid = context.getXid();
            String userId = context.getActionContext("userId").toString();
            AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
            // 空回滚判断,判断 accountFreeze 是否为null,为 null 证明 try 没执行,需要做空回滚
            if(accountFreeze == null){
                accountFreeze = new AccountFreeze();
                accountFreeze.setUserId(userId);
                accountFreeze.setFreezeMoney(0);
                accountFreeze.setState(AccountFreeze.State.CANCEL);
                accountFreeze.setXid(xid);
                accountFreezeMapper.insert(accountFreeze);
                return true;
            }
            // 幂等判断
            if(accountFreeze.getState() == AccountFreeze.State.CANCEL){
                // 如果相等,说明已经处理过一次 CANCEL 了,无需重复处理
                return true;
            }
            // 恢复可用余额
            accountMapper.refund(accountFreeze.getUserId(), accountFreeze.getFreezeMoney());
            // 将冻结金额清空,状态改为 CANCEL
            accountFreeze.setFreezeMoney(0);
            accountFreeze.setState(AccountFreeze.State.CANCEL);
            int i = accountFreezeMapper.updateById(accountFreeze);
            return i == 1;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    4)测试

    测试订单数为 20
    在这里插入图片描述
    控制台信息:
    在这里插入图片描述
    account_freeze_tbl 表
    在这里插入图片描述

    4.4 Saga 模式

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

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

    4.4.1 原理

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

    分布式事务执行过程中,一次执行各参与者的正向操作,如果所有整箱操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,是分布式事务回到初始状态。
    在这里插入图片描述
    Saga 模式是 Seata 提供的长事务解决方案,也分为两个阶段:

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

    Saga 模式与 TCC 模式的区别:
    TCC 模式一阶段不是真正的在处理事务,而是对资源的预留,而 Saga 模式则是直接提交事务,不会对资源进行预留。以 4.3.4 下单业务逻辑为例,TCC 模式在一阶段,除了扣减可用余额外,还会向冻结表中插入冻结的金额,而 Saga 模式则是直接扣减可用余额。
    TCC 二阶段成功,则需要完成业务执行和事务提交,如 4.3.4 中的需要删除冻结表中的冻结金额,而 Saga 则什么都不做。在回滚时,TCC 模式需要完成预留资源的释放,而Saga则是通过编写补偿业务来回滚。

    4.4.2 优缺点

    优点:

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

    缺点:

    • 软状态持续时间不确定,时效性差
    • 没有锁,没有事务隔离,会有脏写

    4.5 四种模式对比

    我们从以下几个方面来对比四种实现:

    • 一致性:能否保证事务的一致性?强一致还是最终一致?
    • 隔离性:事物之间的隔离性如何?
    • 代码侵入:是否需要对业务代码改造?
    • 性能:有误性能损耗?
    • 场景:常见的业务场景
    XAATTCCSAGA
    一致性强一致弱一致弱一致最终一致
    隔离性完全隔离基于全局锁隔离基于资源预留隔离无隔离
    代码侵入有,要编写三个接口有,要编写状态机和补偿业务
    性能非常好非常好
    场景对一致性、隔离性有高要求的业务基于关系型数据库的大多数分布式事务场景都可以对性能要求较高的事务。有非关系型数据局要参与的事务业务流程长、业务流程多。参与者包含其他公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
  • 相关阅读:
    springBoot集成swagger2并使用
    接口自动化测试实践指导(中):接口测试场景有哪些
    torch.utils.data.DataLoader
    Docker学习_存储篇
    电磁场与电磁波part5--均匀平面波在无界空间的传播
    [安卓APP毕业设计]基于android的作业管理app[包运行成功]
    基于Xml方式Bean的配置-beanName个别名配置
    zabbix监控Nginx
    一步一步分析ChatGPT,1 粘性,2 传染性, 3 双边网络效应
    知识图谱推理研究综述9.3
  • 原文地址:https://blog.csdn.net/dingd1234/article/details/126559620