分布式事务之Seata

1. 分布式事务简介

分布式事务是在部署在多个设备上的多个应用间实现要么都成功要么都失败的语义,这些设备一般是通过网络通讯的。

事务的特性

事务的ACID特性, 一个完整的事务要求同时具备以下特性

A(Atomic)原子性,就是数据不能被破坏,是完整的。

C(Consistency)一致性, 数据在执行事务前后要求一致,执行分布在多个表中的数据也要要求一致。不能出现事务完成后。A表和B表数据对应不上的情况。

I(Isolation)事务与事务之间要求数据不能相互干扰,不能看到其他事务运行中的状态,应避免出现脏读,幻读,重复读等问题。

D(Durability)持久性,事务完成后,数据不不会因为断电丢失,应持久化下来,避免回滚。

事务的分类

本地事务:同一数据库和服务器产生的事务,称为本地事务

分布式事务:在分布式环境中,实现了事务,则称为分步式事务

分布式事务理论:

CAP理论

这个定理的内容是指的是在一个分布式系统中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。由于P是必然存在的,所以可以选择AP或CP两种。

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

2. Seata简介&SeataServer安装

1. Seata简介

Seata是有Alibaba主导开发的一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

目前已支持Dubbo、Spring Cloud、Sofa-RPC、Motan 和 gRPC 等RPC框架。

Seata的AT模式提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB。H2、DB2、SQLServer。

支持 TCC 模式并可与 AT 混用,灵活度更高

支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB

支持基于数据库和 Redis 存储的存储计算分离集群模式,水平扩展能力强

SAGA 模式为长事务提供有效的解决方案,为老系统改造提供便捷

2. 2PC即两阶段提交协议

是将整个事务流程分为两个阶段,P(Prepare phase)是指准备阶段,C(Commit phase)是指提交阶段。

准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。

Undo日志是记录修改前的数据,用于数据库回滚

Redo日志是记录修改后的数据,用于提交事务后写入数据文件

提交阶段(commit phase):如果事务管理器收到了参与者的执行成功的消息,那么向事务参与者发送提交(Commit)消息;参与者根据事务管理器的指令执行提交。否则如果收到执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息,并释放事务处理过程中使用的资源。

3. Seata术语

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

图解:通过TM向TC注册并开启全局事务后,调用Stock(RM)执行本地事务,并记录它的回滚日志通知TC。 然后调用Order(RM)执行本地事务,并记录回滚日志通知TC,然后有Order服务调用Account服务,Account记录本地日志后。 通知TC,当TC收到所有的事务参与者(Stock,Order,Account)的执行成功消息,然后就向各个事务参与者发送提交的消息,如果有任何一个服务有失败或超时,TC将向所有的事务参与者发送Rollback的消息。

4. Seata的事务模式

Seata指出AT、XA、TCC和SAGA等事务模式,不同的事务模式是为了解决不同的事务场景下的需求。 注意:不存在某一种分布式事务机制可以完美适应所有场景,满足所有需求。现在,无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。

从事务的运行逻辑来看,可以分为补偿型的事务模式和非补偿型的。

补偿行的有AT,TCC,和SAGA,非补偿性的就是XA。

XA模式也是强一致性的,而其他的则是最终一致性的模式。

XA模式由于是强一致性的,所以可用性并不好,性能也不够高。 所以在大部分互联网应用中一般少采用。 但是XA模式由于是强一致性,必须等所有资源都完成操作后才一起提交给数据库,所以不会产生脏读和重复读的问题。这也是其他模式做不到的。

在Seata 中分布式事务 处理过程,分为两个阶段:

  1. 执行阶段 :执行分支事务,并保证执行结果满足是可回滚的(Rollbackable)持久化的(Durable)
  2. 完成阶段: 根据 执行阶段 结果形成的决议,应用通过 TM 发出的全局提交或回滚的请求给 TC,TC 命令 RM 驱动 分支事务 进行 CommitRollback

而Seata中的不同的事务模式,也就是使用不同的方式达成分布式事务两个阶段的目标。即,回答以下两个问题

  1. 执行阶段如何执行并保证执行结果满足是可回滚的和持久化的
  2. 完成阶段收到 TC 的命令后,是如何做到分支的提交或回滚

例如:

  1. Seata的AT模式,是 在执行阶段是通过undo_log来保证可回滚的,而持久化的话在AT模式下,本身就是先持久化了的。 在完成阶段是通过undo_log来回滚,如果提交,直接删除undo_log就好 AT模式参考图

  2. Seata的XA模式,是 在执行阶段,通过本地事务来保证数据是可回滚的,本地事务卡住不提交就可以了。 因为所有业务都以执行完成,数据库也操作完成,只是还没有提交事务,所以后续如果提交,数据也是可持久化的。 在完成阶段,收到Commit就提交事务,完成持久化操作。如果收到Rollback则回滚事务,这样既相当于请求没有产生过。 XA模式参考图

5. Seata-Server安装

从3小节可以看出,TC是分布式事务的协调者,在Seata中这个也是Seata-Server需要独立安装部署,以提供分布式事务的支持。

而其他TM和RM是作用在Client端的,也就是各个微服务端。

官方下载地址:https://github.com/seata/seata/releases,版本:1.4.2

解压后配置

  1. registry.conf 注册中心和配置中心 修改Seata的注册中心和配置中心为Nacos
registry{
  type = "nacos"
  nacos{
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
config{
  type = "nacos"
  nacos{
    serverAddr = "192.168.48.11:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}
  1. file.conf存储模式 这个步骤可以省略,直接采用下一节将配置放到配置中心,这样以后更改更方便,参考:6. Seata使用配置中心来存放各种配置 修改file.conf文件,把Seata的默认存储模式修改为数据库"DB",同时需要配置JDBC
store {
   mode = "db"
   db {
      datasource = "druid"
      ## mysql/oracle/postgresql/h2/oceanbase etc.
      dbType = "mysql"
      driverClassName = "com.mysql.jdbc.Driver"
      url = "jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true"
      user = "root"
      password = "root"
      minConn = 5
      maxConn = 100
      globalTable = "global_table"
      branchTable = "branch_table"
      lockTable = "lock_table"
      queryLimit = 100
      maxWait = 5000
   }
}
  1. mysql数据库表的建立 从上面的配置可以看出,有一个数据库seata和3张表global_table;branch_table;lock_table是需要建立的。 建表语句下载地址: seata/mysql.sql at develop · seata/seata · GitHub
use seata;
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`)
) 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 ('HandleAllSession', ' ', 0);
  1. 配置完成后启动Seata-server 启动步骤为,先启动nacos然后在启动Seata-Server 启动Seata-Server的方式非常简单,直接双击此文件即可:seata-server-1.4.2\bin\seata-server.bat 启动完成后可以在nacos控制台注册中心就可以看到Seata-Server

6. Seata使用配置中心来存放各种配置

Seata支持注册服务到Nacos,以及支持Seata所有配置放到Nacos配置中心,在Nacos中统一维护;

注意:以下方式在nacos1.4.2之后的版本支持

首先要确保Seata-server配置到注册中心

Seata-server端配置注册中心,在registry.conf中加入配置注册中心nacos 注意:确保client与server的注册处于同一个namespace和group,不然会找不到服务。

registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP" # 这里的配置要和客户端保持一致
    namespace = "" # 这里的配置要和客户端保持一致
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
config {
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}

我们需要把Seata的一些配置上传到Nacos中,配置比较多,所以官方给我们提供了一个config.txt,我们下载并且修改其中参数,并配置到Nacos中 下载地址:https://github.com/seata/seata/tree/develop/script/config-center

修改点

transport.shutdown.wait=3
service.vgroupMapping.mygroup=default # 事务分组
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

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=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30

然后将修改好的配置文件在Nacos配置中心建立。

然后重启Seata-server就可以了

7. Seata集群部署注意点

Seata-Server一般是集群部署的,所以Seata-Server也为集群部署提供了方便的启动参数,可以指定主机;端口,存储模式,节点ID,指定运行环境等。

支持的启动参数

参数全写作用备注
-h--host指定在注册中心注册的 IP不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定
-p--port指定 server 启动的端口默认为 8091
-m--storeMode事务日志存储方式支持 file,db,redis,默认为 file 注:redis需seata-server 1.3版本及以上
-n--serverNode用于指定seata-server节点ID1,2,3..., 默认为 1
-e--seataEnv指定 seata-server 运行环境dev, test 等, 服务启动时会使用 registry-dev.conf 这样的配置

例如:

$ sh ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file

3. Seata中的AT模式

1. AT模式介绍

概念:AT模式是一种无侵入的分布式事务解决方案,在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

AT模式是两阶段提交协议的演变

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。 在一阶段中,Seata会拦截“业务SQL“,首先解析SQL语义,找到要更新的业务数据,在数据被更新前,保存下来"undo",然后执行”业务SQL“更新数据,更新之后再次保存数据”redo“,最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。
  • 二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。 相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的"undo Log",通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。

2. AT模式使用案例

首先设计两个服务模块,一个订单order模块; 一个库存stock模块

1. 库存stock.sql

CREATE SCHEMA `seata-stock` DEFAULT CHARACTER SET utf8mb4 ;
CREATE TABLE `seata-stock`.`stock` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `product_id` INT NULL,
  `count` INT NULL,
  `money` INT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;
CREATE TABLE `seata-stock`.`undo_log` (
  `branch_id` bigint(20) 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 DEFAULT CHARSET=utf8mb4 COMMENT='AT transaction mode undo table';
INSERT INTO `seata-stock`.`stock` (`id`, `product_id`, `count`, `money`) VALUES ('1', '1', '100', '1');



2. 订单order.sql

CREATE SCHEMA `seata-order` DEFAULT CHARACTER SET utf8mb4 ;
CREATE TABLE `seata-order`.`order` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `product_id` INT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;
CREATE TABLE `seata-order`.`undo_log` (
  `branch_id` bigint(20) 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 DEFAULT CHARSET=utf8mb4 COMMENT='AT transaction mode undo table';


3. order模块创建

订单模块负责接收外部请求,创建本地开启全局事务,并创建订单,然后远程调用库存模块,扣减库存。

服务order配置application.properties

server.port=8801
spring.application.name=seata-order

# 服务注册也发现相关配置
spring.cloud.nacos.discovery.server-addr=192.168.43.11:8848
management.endpoint.web.exposure.include=*

# 数据库相关配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.43.11:30306/seata-order?useUnicode=true&rewriteBatchedStatements=true&useSSL=false&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

# Mybatis相关配置
mybatis.mapper-locations=classpath:/mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true

# Seata相关配置
spring.cloud.alibaba.seata.tx-service-group=tx_group
seata.tx-service-group=default_tx_group
seata.service.vgroup-mapping.default_tx_group=default

pom.xml,具体下载源码参考

<!--注入spring-boot-starter-web相关包-->
<!--注入阿里巴巴nacos服务注册发现相关包-->
<!-- mysql:mysql驱动 -->
<!-- mysql:MyBatis相关依赖 -->
<!-- lombok相关依赖 -->
<!-- spring-cloud-starter-alibaba-seata相关依赖 -->

4. order模块核心代码

//SeataOrderApplication.java
@SpringBootApplication
//开启Nacos注册中心,方便后续使用OpenFeign远程调用服务
@EnableDiscoveryClient
//开启OpenFeign,支持@FeignClient("seata-stock")远程调用
@EnableFeignClients
public class SeataOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderApplication.class, args);
    }
}

//OrderController.java
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/order/create")
    public String create(Integer productId) {
        orderService.create(productId);
        return "生成订单";
    }
}

//OrderServiceImpl.java
@Service
public class OrderServiceImpl implements OrderService {
    
    @Resource
    private OrderDao orderDao;
    
    @Resource
    private StockClient stockClient;
    
    @Override
    // 开启Seata分布式全局事务,默认AT,可以通过配置成XA模式
    @GlobalTransactional
    public void create(Integer productId) {
        // 远程调用库存模块减库存
        stockClient.decrement(productId);
        //本地数据库操作添加订单,SeataAT模式下本地无需事务
        Order order = new Order();
        order.setProductId(productId);
        orderDao.insert(order);
        // 添加异常,异常后,理论上库存模块回滚,本地数据库操作也回滚
        // int i = 1 / 0;
    }
}

5. stock模块创建

库存模块,负责接收来自订单模块的扣减库存请求,并扣减指定商品的库存数。

application.properties

server.port=8802
spring.application.name=seata-stock

# 服务注册也发现相关配置
spring.cloud.nacos.discovery.server-addr=192.168.43.11:8848
management.endpoint.web.exposure.include=*

# 数据库相关配置11
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.43.11:30306/seata-order?useUnicode=true&rewriteBatchedStatements=true&useSSL=false&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

# Mybatis相关配置
mybatis.mapper-locations=classpath:/mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true

# Seata相关配置
spring.cloud.alibaba.seata.tx-service-group=tx_group
seata.tx-service-group=default_tx_group
seata.service.vgroup-mapping.default_tx_group=default

pom.xml,具体下载源码参考

<!--注入spring-boot-starter-web相关包-->
<!--注入阿里巴巴nacos服务注册发现相关包-->
<!-- mysql:mysql驱动 -->
<!-- mysql:MyBatis相关依赖 -->
<!-- lombok相关依赖 -->
<!-- spring-cloud-starter-alibaba-seata相关依赖 -->

6. stock模块核心代码

@SpringBootApplication
@EnableDiscoveryClient
public class SeataStockApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeataStockApplication.class, args);
    }
}

@RestController
public class StockClientController {

    @Autowired
    private StockService stockService;

    @GetMapping("/decrement/{productId}")
    public String decrement(@PathVariable("productId") Integer productId) {
        //SeataAT模式下,无需本地事务
        stockService.decrement(productId);
        return "Decremented";
    }
}

7. 测试结果说明

  1. 启动mysql-server
  2. 启动Nacos-Server
  3. 启动Seata-Server
  4. 启动Stock模块
  5. 启动Order模块
  6. 请求地址:http://localhost:8801/order/create?productId=1
  7. 在OrderService没有异常int i = 1 / 0;时全部执行通过
  8. 在OrderService有异常int i = 1 / 0;时,异常后,order库和stock库都没有变化
  9. 在在OrderService的异常处int i = 1 / 0;打断点,运行到此处时,数据库order和stock库中有undo_log记录,并且有新订单产生和库存已扣减。 但放过断点并异常后,数据库undo_log记录消失,并且新订单消失和库存已还原。

4. Seata中的XA模式

Seata 1.2.0 版本开始支持XA 模式

1. 什么是XA事务

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准

XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范 的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。

XA 规范 在上世纪 90 年代初就被提出,用以解决分布式事务处理这个领域的问题。

目前,几乎所有主流的数据库都对 XA 规范 提供了支持。

2. DTP模型定义如下角色

  1. AP:即应用程序,可以理解为使用DTP分布式事务的程序
  2. RM:资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库的实例(MySql),通过资源管理器对该数据库进行控制,资源管理器控制着分支事务
  3. TM:事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理实务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
  4. DTP模式定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现的2PC又称为XA方案。

3. XA方案拓扑图

4. XA协议的痛点

如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。

这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。

5. Seata中的XA模式

Seata中的XA使用来说和AT模式差不多,XA模式还是使用Seata中还是TM、TC、RM等角色。 只是对于XA来说是要求数据库一定要支持事务,XA模式下事务是要等TC的Commit消息后才提交事务。 而AT模式是先提交的事务。 后续如果需要的话通过undo_log来进行回滚操作。

执行步骤如下:

  1. TM 向 TC 请求发起(Begin Global Transaction),生成全局事务XID
  2. TM 把代表全局事务的 XID 绑定到分支事务上
  3. RM 向 TC 注册,把分支事务关联到 XID 代表的全局事务中。
  4. RM 把分支事务的执行结果上报给 TC,如果未上报,超时后TC发送回滚消息
  5. TC 发送分支提交(Branch Commit)或分支回滚(Branch Rollback)命令给 RM。

参考图:

6. Seata中XA存在的价值

在Seata中其实AT模式已经能解决80%是的分布式事务问题了, 那为什么要在 Seata 中增加 XA 模式呢?支持 XA 的意义在哪里呢?

因为本质上,Seata 已经支持的 3 大事务模式:AT、TCC、Saga 都是 补偿型 的。

补偿型 事务处理机制构建在 事务资源 之上(要么在中间件层面,要么在应用层面),事务资源 本身对分布式事务是无感知的。事务资源 对分布式事务的无感知存在一个根本性的问题:无法做到真正的 全局一致性 。

比如,一条库存记录,处在 补偿型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连接数据库,查询统计库存,就看到当前的 50。之后,事务因为异外回滚,库存会被补偿回滚为 100。显然,仓库管理员查询统计到的 50 就是 脏 数据。所以补偿型事务是存在中间状态的(中途可能读到脏数据)

而XA模式刚好能解决这个问题。因为XA是非补偿行模式,XA 协议 要求 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。

比如,刚才提到的库存更新场景,XA 事务处理过程中,中间状态数据库存 50 由数据库本身保证,是不会仓库管理员的查询统计看到的。

除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:

  1. 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
  2. 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
  3. 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少。
  4. 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑

7. Seata XA模式的使用

Seata XA模式的使用和AT模式并没有多大差别,唯一区别的地方就是需要指定数据源代理类。 由于默认的情况下,Seata是自动代理了数据源,并且代理类被指定成了AT模式的代理类,所以AT模式不需要单独配置。 而XA是需要配置数据源代理,所以需要关闭自动代理,通过代码来指定XA代理。当然也可以通过代码指定成AT模式的代理类

经测试,XA模式也不需要开启本地事务的,应该是@GlobalTransactional会自动感知,并提前开启本地事务。

官方参考例子

seata-samples/seata-xa at master · seata/seata-samples (github.com)

XA模式实战

  1. 设计数据库和模块设计参考上一节AT模式
  2. 关闭自动代理配置
# 由于默认情况下是自动代理成AT模式的,如需要自定义模式,需要关闭数据源代理
# 如需要启用XA模式时,先关闭自动代理,然后由代码来配置XA数据源代理
seata.enable-auto-data-source-proxy=false
  1. 配置Seata数据源代理 这个配置类很重要,我折腾了很久才搞明白,官方的例子是基于JDBC的,而我给出的例子是基于mybatis的,官方的代码由于不是构建在新的版本之上,参考官方的在新版本构建出现各种问题。 以下是我给出新的版本能运行的数据源配置。
@Configuration
public class SeataDataSourceConfig {

    @Resource
    private DataSourceProperties dataSourceProperties;

    // 这里必须要以构建的方式创建druidDataSource源才可以
    // 因为框架需要注入datasource必须要是被代理好的,如果用官方的方法会有问题,如下:
    // 备注:官方的方法也许在官方的代码依赖的版本应该是可以的,我用的新点的版本,具体参考代码
    // 官方的方法:
    //    1. 先用框架的能力创建数据源注入到spring容器中
    //       也就是直接返回new DruidDataSource();后,由框架处理配置信息
    //    2. 然后通过另一个方法注入这个DataSource,重新创建一个代理数据源Bean,这样就会出现循环依赖问题
    //       The dependencies of some of the beans in the application context form a cycle
    //    3. 官方配置参考:https://github.com/seata/seata-samples/blob/master/seata-xa/order-xa/src/main/java/io/seata/sample/OrderXADataSourceConfiguration.java
    // 实战使用方法:
    //    1. 通过一个方法创建好代理好的dataSource
    //    2. 然后通过通过Seata的数据源代理类代理数据源后,再注入到spring容器中
    @Bean
    public DataSource dataSource() {
        DruidDataSource druidDataSource = createDruidDataSource();
        //这里可以实现动态切换数据源为XA还是AT模式,只需要返回不同的代理即可
        return new DataSourceProxyXA(druidDataSource);
        //return new DataSourceProxy(druidDataSource);
    }

    private DruidDataSource createDruidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());
        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setMaxActive(180);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMinIdle(0);
        druidDataSource.setValidationQuery("SELECT 1 FROM DUAL");
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(25200000);
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(1800);
        druidDataSource.setLogAbandoned(true);
        return druidDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource,
                                               @Value("${mybatis.mapper-locations}")
                                                       String mapperLocations) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(mapperLocations));
        return factoryBean.getObject();
    }
}
  1. 配置的pom.xml中的关键点
<!--注入阿里巴巴nacos服务注册发现相关包-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- mysql:mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>
<!-- mysql:MyBatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<!-- seata: 分布式事务支持 -->-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

注意问题点

  1. 在实战过程中发现mysql5.7在xa模式下有问题,会不断的爆出异常(XAER_NOTA: Unknown XID),具体可参考:MySQL 5.7复制的一个小bug-XA事务 - 简书解决办法:所以我就改用了mysql8.0.28版本后就没再出这个问题
  2. 使用新版的数据库需要使用新版的数据库驱动,但是最新的驱动(8.0.28)会报错,如下: atomikos 异常报错(java.lang.NoSuchMethodException:com.mysql.cj.conf.PropertySet.getBooleanReadablePrope) 解决办法:将mysql驱动切换为较低的版本8.0.11
  3. 在使用了新版的数据库后,连接地址的各种参数最好都需要加上,比如连接参数allowPublicKeyRetrieval没有的话,就会报错:Public Key Retrieval is not allowed,参考连接URL如下: spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true

8. Seata XA模式实战代码

需要提前准备:

  1. 启动nacos 我的是192.168.43.11:8848
  2. 启动mysql数据库seata-order、seata-stock
  3. 启动seata-server,需要配置到注册中心nocos

下载:分布式事务之SeataXAAT实战代码-行业报告文档类资源-CSDN下载

9. Seata XA模式,如何测试

  1. 请求地址:http://localhost:8801/order/create?productId=1
  2. 在OrderService没有异常int i = 1 / 0;时全部执行通过
  3. 在OrderService有异常int i = 1 / 0;时,异常后,order库和stock库都没有变化
  4. 在OrderService的异常处int i = 1 / 0;打断点,运行到此处时,查看数据库seata-order的order表没有新增记录,数据库seata-stock中的stock表也没有减少库存数,则XA生效。
  5. 注释异常后,在异常前打断点,然后此时数据库是没有变更的,放过异常正常执行完成业务后,order表有新增记录,stock表有扣减库存。

5. Seata中的TCC模式和SAGA模式

1. 什么是TCC

TCC 是分布式事务中的二阶段提交协议,是一种补偿性的分布式事务解决方案,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:

  1. Try:对业务资源的检查并预留;
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。

TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖于数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。

2. TCC和AT的区别

AT模式是基于数据库自身的ACID支持来实现,在一阶段时,利用本地事务支持,将业务数据更新和相应回滚日志一并提交到数据库,在二阶段利用回滚日志进行回滚或删除日志提交。

而TCC模式不需要持久层是数据库,也就是不需要事务支持,比如Redis,Kafka等都可以做为目标存储,在一阶段只需要将业务数据提交到数据库,在二阶段如果需要回滚,就进行反向补偿,或者执行提交业务,所以二阶段的逻辑需要开发人员自己写出。

因为要自己写回滚和提交逻辑,所以TCC是对业务系统有着非常大的入侵性,设计相对复杂的。

但由于不需要事务,所以整个过程没有锁,没有其他开销,所以性能也是比较高的,因为需要回滚的场景在正常的业务一般都是很少的。

总结TCC的特点如下:

  1. 侵入性比较强,并且需要自己实现相关事务控制逻辑
  2. 在整个过程基本没有锁,性能较强

3. 什么是SAGA模式

SAGA模式一般很少在分布式事务中使用。

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段反向补偿服务(执行处理时候出错了,给一个修复的机会)都由业务开发实现。

Saga模式提供了异构系统的事务统一处理模型。在Saga模式中,所有的子业务都不在直接参与整体事务的处理(只负责本地事务的处理),而是全部交由了最终调用端来负责实现,而在进行总业务逻辑处理时,在某一个子业务出现问题时,则自动补偿全面已经成功的其他参与者,这样一阶段的正向服务调用和二阶段的服务补偿处理全部由总业务开发实现。

Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。

由于SAGA不常用,就不在赘述了,有兴趣可以参考官方文档

官方文档地址:Seata Saga 模式

Seata Safa状态机可视化图形设计器使用地址:https://github.com/seata/seata/blob/develop/saga/seata-saga-statemachine-designer/README.zh-CN.md

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值