分布式事务框架 Seata 详解(附代码)

Seata

文章目录

  • Seata
    • 分布式事务介绍
    • 两阶段提交协议(2PC)
      • 角色
        • TC (Transaction Coordinator) - 事务协调者
        • TM (Transaction Manager) - 事务管理器
        • RM (Resource Manager) - 资源管理器
      • 概念
      • 2PC 存在的问题
    • Seata 分布式事务框架
      • 简介
      • Seata 分布式事务生命周期
      • SQL限制
        • 使用限制
      • 快速开始
        • TC 端环境搭建
        • Client(TM / RM) 端搭建(AT为例)
      • 事务模式
        • AT 模式(推荐)
          • 前提
          • 整体机制
          • 设计思路
          • 大致流程
          • 快速使用
          • 回滚失败异常处理
          • 注意事项
        • XA 事务模式
          • 前提
          • 整体机制
          • 与 AT 模式的区别
          • 快速使用
        • TCC 模式
          • 前提
          • 整体机制
          • TCC 异常控制机制
          • 与 AT 等模式的区别
          • 快速使用
        • Saga模式
      • 其他补充
        • 除 AT 模式外二阶段只有成功 / 失败重试两种逻辑?
        • TCC 模式异常重试机制
      • 示例项目代码地址

分布式事务介绍

在微服务架构中,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一 致性。

两阶段提交协议(2PC)

角色

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

概念

两阶段提交(Two Phase Commit),就是将提交(commit)过程划分为两个阶段(Phase):

  • 阶段1
    TM通知各个RM准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。
    mysql数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出==prepare"准备提交"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成commit"可以提交"==,然后把结果返回给事务管理器。
  • 阶段2
    TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RMprepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。
    mysql数据库为例,如果第一阶段中所有数据库都prepare成功,那么事务管理器向数据库服务器发出=="确认提交"请求,数据库服务器把事务的"可以提交"状态改为"提交完成"状态==,然后返回应答。如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会 把=="可以提交"的事务回撤==

分布式事务框架 Seata 详解(附代码)_第1张图片

两阶段提交方案下全局事务的ACID特性,是依赖于RM的。一个全局事务内部包含了多个独立的事务分支,这一组事务分支要么都成功,要么都失败。各个事务分支的ACID特性共同构成了全局事务的ACID特性。也就是将单个事务分支支持的ACID特性提升一个层次到分布式事务的范畴。

2PC 存在的问题

同步阻塞问题

2PC 中的参与者是阻塞的。在第一阶段收到请求后就会预先锁定资源,一直到 commit 后才会释放。

单点故障

由于协调者的重要性,一旦协调者TM发生故障,参与者RM会一直阻塞下去。尤其在第二阶段, 协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。

数据不一致

若协调者第二阶段发送提交请求时崩溃,可能部分参与者收到commit请求提交了事务,而另一 部分参与者未收到commit请求而放弃事务,从而造成数据不一致的问题。

Seata 分布式事务框架

简介

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)

官网:https://seata.io/zh-cn/index.html

源码: https://github.com/seata/seata

Seata 分布式事务生命周期

Seata 中,一个分布式事务的生命周期如下:

  1. TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID会在微服 务的调用链路中传播,保证将多个微服务的子事务关联在一起。
  2. RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
  3. TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
  4. TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。

SQL限制

Seata 事务目前支持 INSERT、UPDATE、DELETE 三类 DML 语法的部分功能,这些类型都是已经经过 Seata 开源社区的验证。SQL 的支持范围还在不断扩大,建议在本文限制的范围内使用。如果您有意帮助社区支持更多类型的 SQL,请提交 PR 申请。

使用限制
  • 不支持 SQL 嵌套
  • 不支持多表复杂 SQL(自1.6.0版本,MySQL支持UPDATE JOIN语句,详情请看 )
  • 不支持存储过程、触发器
  • 部分数据库不支持批量更新,在使用 MySQL、Mariadb、PostgreSQL9.6+作为数据库时支持批量,批量更新方式如下以 Java 为例
    // use JdbcTemplate
    public void batchUpdate() {
        jdbcTemplate.batchUpdate(
            "update storage_tbl set count = count -1 where id = 1",
            "update storage_tbl set count = count -1 where id = 2"
		);
    }

    // use Statement
    public void batchUpdateTwo() {
        statement.addBatch("update storage_tbl set count = count -1 where id = 1");
        statement.addBatch("update storage_tbl set count = count -1 where id = 2");
        statement.executeBatch();
    }

快速开始

下载:seata.io

SeataTCTMRM三个角色,TC(Server端)为单独服务端部署,TMRM(Client端)由业务系统集成。

资源目录:

  • https://github.com/seata/seata/tree/v1.5.1/script
  • client :存放clientsql脚本,参数配置
  • config-center :存放各个配置中心参数导入脚本,config.txt(包含serverclient)为通用参数文件
  • server :存放数据库脚本及各个容器配置
TC 端环境搭建

修改配置文件

配置文件的位置在/seata/config

分布式事务框架 Seata 详解(附代码)_第2张图片

配置中心设置

如果nacos上的配置与application.yml有相同配置,则覆盖

示例配置

分布式事务框架 Seata 详解(附代码)_第3张图片

实际配置(使用nacos

分布式事务框架 Seata 详解(附代码)_第4张图片

nacos上传配置文件

直接官方提供的即可,\script\config-center\config.txt,复制文件内容,在nacos上按照上述配置创建配置文件

分布式事务框架 Seata 详解(附代码)_第5张图片

TC 端注册中心设置

实际配置(使用nacos

分布式事务框架 Seata 详解(附代码)_第6张图片

日志配置

分布式事务框架 Seata 详解(附代码)_第7张图片

存储模式配置

Server端存储模式(store.mode)支持三种:

  • file :单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高
  • db :高可用模式,全局事务会话信息通过db共享,相应性能差些
    目前仅支持 mysql、oracle、postgresql
  • redis :1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置

实际配置(使用db

naocs配置中心修改配置文件

分布式事务框架 Seata 详解(附代码)_第8张图片

分布式事务框架 Seata 详解(附代码)_第9张图片

由于seata是通过jdbcexecuteBatch来批量插入全局锁的,根据MySQL官网的说明,连接参数中的rewriteBatchedStatementstrue时,在执行executeBatch,并且操作类型为insert时,jdbc驱动会把对应的SQL优化成insert into () values (), ()的形式来提升批量插入的性能。根据实际的测试,该参数设置为true后,对应的批量插入性能为原来的10倍多,因此在数据源为MySQL时,建议把该参数设置为true

创建需要的表(mysql)

在配置中的数据库下,执行\seata\script\server\db\mysql.sql文件即可

分布式事务框架 Seata 详解(附代码)_第10张图片

事务分组设置

配置事务分组, 之后client端配置需与其的事务分组一致

  • 事务分组seata的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
  • 集群seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与Seata服务端集群的映射关系。

naocs配置中心修改配置文件

分布式事务框架 Seata 详解(附代码)_第11张图片

事务分组如何找到后端Seata集群(TC)?

  1. 首先应用程序(客户端)中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数)。若应用程序是SpringBoot则通过seata.tx-service-group配置。
  2. 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping .[事务分组配置项],取得配置项的值就是TC集群的名称。若应用程序是SpringBoot则通过seata.service.vgroup-mapping.事务分组名=集群名称 配置
  3. 拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同(前提是Seata-Server已经完成服务注册,且Seata-Server向注册中心报告cluster名与应用程序(客户端)配置的集群名称一致)
  4. 拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表(即Seata-Server集群节点列表)

启动Seata Server

双击/bin/seata-server.bat启动

分布式事务框架 Seata 详解(附代码)_第12张图片

nacos 上注册成功

分布式事务框架 Seata 详解(附代码)_第13张图片

支持的启动参数

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

比如:

bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db
Client(TM / RM) 端搭建(AT为例)

AT模式依赖于seataundo_log回滚日志,来进行事务的回滚。所以需要为每个微服务的对应的库创建undo_log日志表(如果都在同一个库则仅需要一张undo_log表即可)。

-- for AT mode you must to init this sql for you business database. the seata server not need it.
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 = utf8mb4 COMMENT ='AT transaction mode undo table';

TM 端

maven 依赖

<dependencies>

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>

    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>

    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
        <scope>providedscope>
    dependency>

    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>

    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
    dependency>

    
    
    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        <exclusions>
            <exclusion>
                <groupId>io.seatagroupId>
                <artifactId>seata-spring-boot-starterartifactId>
            exclusion>
        exclusions>
    dependency>
    
    <dependency>
        <groupId>io.seatagroupId>
        <artifactId>seata-spring-boot-starterartifactId>
        <version>1.6.0version>
    dependency>


    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
    dependency>

dependencies>

application.tml 配置

server:
  port: 7100

spring:
  application:
    name: xa-order
  cloud:
    # 应用自身nacos注册地址
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        password: nacos
        username: nacos
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root

# seata配置需保持和server端一致
seata:
  application‐id: ${spring.application.name}
  # 数据源代理模式 默认AT
  data-source-proxy-mode: XA
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx‐service‐group: default_tx_group
  # TC注册中心配置
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ad50a46c-e62f-4aa6-9ad2-1b1edbaaee03
  client:
    # tm端配置
    tm:
      # 一阶段全局提交结果上报TC重试次数 (配置值使用的是默认值)
      commit-retry-count: 5
      # 一阶段全局回滚结果上报TC重试次数 (配置值使用的是默认值)
      rollback-retry-count: 5
      # 全局事务超时时间 (配置值使用的是默认值)
      default-global-transaction-timeout: 6000
      # TM全局事务拦截器顺序 (配置值使用的是默认值)
      # 保证拦截器在本地事务拦截器之前执行,也可自定义全局事务和业务开发的拦截器执行顺序
      interceptor-order: -2147482648
      # 分布式事务降级开关
      degrade-check: false

业务代码有@GlobalTransactional的即为TM

RM 端

RM端和TM端一致,如果不充当TM则不需要使用cloud整合依赖


<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.6.0version>
dependency>

事务模式

AT 模式(推荐)
前提
  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。
整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。
设计思路

Seata AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如下:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

一阶段

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undo_log,并入库。

分布式事务框架 Seata 详解(附代码)_第14张图片

二阶段

分布式事务操作成功,则TC通知RM异步删除undo_log

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBTpzDN4-1673579586624)(https://typroa-note-images.oss-cn-beijing.aliyuncs.com/img/clipboard%20(1)].png)

分布式事务操作失败,TMTC发送回滚请求,RM收到协调器TC发来的回滚请求,通过 XIDBranch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iNVDryyE-1673579586625)(https://typroa-note-images.oss-cn-beijing.aliyuncs.com/img/clipboard%20(2)].png)

大致流程
  1. TM发起全局事务开启请求TCTC返回XID,并构建全局事务信息存储到global_table表中。
  2. TM端执行业务方法,并将XID向下游远程调用传递。
  3. 各个RM端执行自身的事务方法,并记录到seataundo_log中。并向TC提交一阶段的分支事务记录
  4. 整个过程无异常,TM发起全局事务提交请求TC
    1. TC释放全局锁,删除对应的全局锁记录,更新全局事务状态。
    2. TC向各分支事务发起异步二阶段分支事务提交
    3. RM异步提交删除之前的undo_log日志任务到队列,返回二阶段提交完成状态。
  5. 整个过程出现异常,TM发起全局事务回滚请求TC
    1. TC修改全局事务状态:Begin—>Rollbacking
    2. TC向各分支事务发起远程调用,通知RM删除对应undo_log
    3. RM收到通知,校验undo_log(数据的前后镜像对比)
      1. 成功:分支事务二阶段回滚成功
      2. 失败:分支事务二阶段回滚失败
        1. 镜像不一致失败,不进行重试
        2. 网络等其他原因,进行重试
    4. TC收到各分支事务的响应,返回全局事务状态给TM
  6. TM收到TC响应的全局事务状态,若则失败根据事务状态进行处理
快速使用

按照上面的 Client端搭建即可。AT模式对业务代码几乎无侵入,仅需要在对应的TM端加上@GlobalTransactional注解即可。

回滚失败异常处理

Seata提供了 FailureHandler 可扩展接口,可以让开发自行处理一些提交或回滚失败后的处理。
作用于 TM端,利用各分支事务二阶段处理结果返回给TCTC再将二阶段事务执行结果返回给TM

import io.seata.tm.api.DefaultFailureHandlerImpl;
import io.seata.tm.api.FailureHandler;
import io.seata.tm.api.GlobalTransaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
@Slf4j
public class SeataFailureHandlerConfig {

    @Bean
    public FailureHandler failureHandler(){
        return new EmailSeataFailureHandler();
    }

    class EmailSeataFailureHandler extends DefaultFailureHandlerImpl {

        @Override
        public void onBeginFailure(GlobalTransaction tx, Throwable cause) {
            super.onBeginFailure(tx, cause);
            log.warn("邮件通知:分布式事物出现异常:[onBeginFailure],xid:[{}]", tx.getXid());
        }

        @Override
        public void onCommitFailure(GlobalTransaction tx, Throwable cause) {
            super.onCommitFailure(tx, cause);
            log.warn("邮件通知:分布式事物出现异常:[onCommitFailure],xid:[{}]", tx.getXid());
        }

        @Override
        public void onRollbackFailure(GlobalTransaction tx, Throwable originalException) {
            super.onRollbackFailure(tx, originalException);
            log.warn("邮件通知:分布式事物出现异常:[onRollbackFailure],xid:[{}]", tx.getXid());
        }

        @Override
        public void onRollbackRetrying(GlobalTransaction tx, Throwable originalException) {
            super.onRollbackRetrying(tx, originalException);
            log.warn("邮件通知:分布式事物出现异常:[onRollbackRetrying],xid:[{}]", tx.getXid());
        }
    }
}
注意事项

AT模式的回滚利用的是undo_log日志,所以如果在undo_log生成后,回滚前,这条数据被其他业务或人为所修改,则无法进行回滚(前后镜像不一致);且全局事务锁不会释放,那么意味着这条业务线在数据修正前将一直无法使用。

解决方案

  1. 利用上述的重写FailureHandler进行邮件、短信等通知人为及时处理
  2. 避免在全局事务执行期间RM端涉及业务数据被其他业务修改
    如果是同方法下该RM必须被多个业务调用,则可以在该业务方法上使用@GlobalTransactional注解进行全局事务锁控制,这样就控制业务逐一执行,避免脏数据产生。
XA 事务模式
前提
  • 支持 XA 事务的数据库。
  • Java 应用,通过 JDBC 访问数据库。
整体机制

在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

分布式事务框架 Seata 详解(附代码)_第15张图片

  • 执行阶段:
    *
    • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
    • 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
  • 完成阶段:
    *
    • 分支提交:执行 XA 分支的 commit
    • 分支回滚:执行 XA 分支的 rollback

详见官网介绍

与 AT 模式的区别
  1. AT的前提是支持ACID的关系型数据库。
    XA的前提是支持XA事务的数据库。
  2. XA模式的分支事务会一直等待TMTC响应执行结果,再进行回滚或提交,在这期间由事务产生的锁是一直占用资源的。
    AT模式则是异步化的,根据undo_log进行回滚。
  3. XA的分支事务注册由TC统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前(未来也许会向AT模式一样)。
    AT 模式则是在本地事务提交之前才注册分支,可以避免分支执行失败的情况下进行无意义的分支注册。
  4. XA模式代码需要利用本地事务@Transactional注解,否则方法内的本地事务执行sql会有事务冲突,造成死锁
快速使用

相比较 AT 模式需要做的改动仅有两点:

  1. TM端需要加上Spring@Transactional注解
@Override
@Transactional
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo, Boolean hasException) {
    log.info("=============用户下单=================");
    log.info("当前 XID: {}", RootContext.getXID());
    // ...
}
  1. yml配置文件中指定数据源代理模式为 XA

    # seata配置需保持和server端一致
    seata:
      application‐id: ${spring.application.name}
      # 数据源代理模式 默认AT
      data-source-proxy-mode: XA
      # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
      tx‐service‐group: default_tx_group
    
TCC 模式
前提

TCC 模式,不依赖于底层数据资源的事务支持。它是一种手动控制的模式,可以应用在各种数据库中,例:Redis等。

整体机制

一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commitrollback 行为

TCC 模式不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。

分布式事务框架 Seata 详解(附代码)_第16张图片

TCC 异常控制机制

TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题。

Seata 的做法是在客户端新增一个 TCC 事务控制表( tcc_fence_log ),里面记录了分支事务一阶段操作的 XID、BranchId 及执行状态等,后续会根据这个记录处理各异常。

空回滚

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。

产生原因

分布式事务框架 Seata 详解(附代码)_第17张图片

如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机、网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。

解决

根据 tcc_fence_log 的记录,在执行 Cancel / Rollback 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

幂等

幂等问题指的是 TC 重复进行二阶段提交,因此 Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。

产生原因

分布式事务框架 Seata 详解(附代码)_第18张图片

如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。

解决

根据 tcc_fence_log 记录状态的字段 status,该字段有 4 个值,分别为:

  1. tried:1
  2. committed:2
  3. rollbacked:3
  4. suspend:4

二阶段 Confirm / Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

产生原因

分布式事务框架 Seata 详解(附代码)_第19张图片

如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。

解决

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行。

与 AT 等模式的区别
  1. AT等模式需要依赖数据库的事务特性
    TCC模式则是通过调用自定义的逻辑进行事务控制
  2. AT等模式对业务的侵入几乎为0
    TCC模式由于是调用自定义的逻辑,所以对业务有较大的的侵入
  3. AT模式利用了undo_log的镜像记录进行自动回滚处理
    TCC模式调用自定义的回滚逻辑,不过针对一些特殊的异常利用了tcc_fence_log 记录表
快速使用

TCC模式与之前两种的模式实现上有较大的区别,主要在于其是通过调用自定义的逻辑去实现事务控制

客户端建立 tcc_fence_log 表

TCC模式对一些特殊的异常处理依赖于tcc_fence_log表,所以需要在各客户端建立该表。(若不考虑这些特殊异常可以不使用)

-- 创建 tcc_fence_log 表,支持 tcc 解决空回滚、悬挂、幂等问题
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
    `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
    `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

配置文件中也可以指定表名

# seata配置需保持和server端一致
seata:
  application‐id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx‐service‐group: default_tx_group
  # tcc 配置
  tcc:
    fence:
      # 表名配置
      log-table-name: tcc_fence_log

TCC 接口编写

TCC模式依赖于自定义逻辑,所以需要自己去定义一阶段的预提交,二阶段的回滚/提交方法

注意: commitrollback方法必须有BusinessActionContext,否则无法获取到上下文数据(除非不需要利用上下文数据)

import cn.zh.order.domain.vo.OrderVo;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author zh
 * OrderService对应的Tcc接口
 */
@LocalTCC
public interface OrderServiceSeataTcc {

    /**
     * TCC的try方法:保存订单信息,状态为支付中
     *
     * 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
     *  name = 该tcc的bean名称,全局唯一
     *  commitMethod = commit 为二阶段确认方法
     *  rollbackMethod = rollback 为二阶段取消方法
     *  BusinessActionContextParameter注解 传递参数到二阶段中
     *  useTCCFence seata1.5.1的新特性,用于解决TCC幂等,悬挂,空回滚问题,需增加日志表tcc_fence_log
     *
     * @param orderVo
     * @return
     */
    @TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
    Integer prepareSaveOrder(OrderVo orderVo);

    /**
     *
     * TCC的confirm方法:订单状态改为支付成功
     *
     * 二阶段确认方法可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param actionContext
     * @return
     */
    void commit(BusinessActionContext actionContext);

    /**
     * TCC的cancel方法:订单状态改为支付失败
     * 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
     *
     * @param actionContext
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);

}

注解 / 参数说明

  • @LocalTCC
    声明TCC接口Bean。适用于SpringCloud+Feign模式下,@LocalTCC需要注解在接口上,此接口可以是寻常的业务接口(并不需要一定独立),只要实现了TCC的两阶段提交对应方法便可。

  • @TwoPhaseBusinessAction
    作用于一阶段预提交方法,常用参数:

    • name:当前tcc方法的bean名称,需保证全局唯一
    • commitMethod:指向当前接口中的提交方法,默认方法名 commit
    • rollbackMethod:指向当前接口的回滚方法,默认方法名 rollback

    定义完三个方法后,seata会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。

  • @BusinessActionContextParameter
    作用于预提交方法的参数上,被注解的参数会被传入 BusinessActionContextTCC的业务上下文),这个类会被自动传递到另外两个方法

  • BusinessActionContext
    TCC的业务上下文,内部利用Map结构对数据存储。

    public class BusinessActionContext implements Serializable {
    
        private static final long serialVersionUID = 6539226288677737991L;
    
        private String xid;
    
        private String branchId;
    
        private String actionName;
    
        private Boolean isDelayReport;
    
    
    
          private Boolean isUpdated;
    
    
          private Map<String, Object> actionContext;
          
          // ...
      }
    

    此外数据传递是利用了BusinessActionContextUtil,这个工具类利用ThreadLocalBusinessActionContext进行数据传递,所以我们也可不使用@BusinessActionContextParameter,而直接使用BusinessActionContextUtil进行参数传递

    public final class BusinessActionContextUtil {
    
        private BusinessActionContextUtil() {
        }
    
        private static final Logger LOGGER = LoggerFactory.getLogger(BusinessActionContextUtil.class);
    
        // ThreadLocal
        private static final ThreadLocal<BusinessActionContext> CONTEXT_HOLDER = new ThreadLocal<>();
    
        public static boolean addContext(String key, Object value) {
            if (value == null) {
                return false;
            }
    
            Map<String, Object> newContext = Collections.singletonMap(key, value);
            return addContext(newContext);
        }
        
        // ...
    }
    
    // 例子
    @Override
    public Integer prepareSaveOrder(OrderVo orderVo) {
    
        // 保存订单
        Order order = new Order();
        order.setUserId(orderVo.getUserId());
        order.setCommodityCode(orderVo.getCommodityCode());
        order.setCount(orderVo.getCount());
        order.setMoney(orderVo.getMoney());
        order.setStatus(OrderStatus.INIT.getValue());
        Integer saveOrderRecord = orderMapper.insert(order);
        log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
    
        // 内部添加上下文数据
        BusinessActionContextUtil.addContext("orderId", order.getId());
        return saveOrderRecord;
    }
    

TM 端方式使用 @GlobalTransactional 注解

@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public void saveOrder(OrderVo orderVo, Boolean hasException) {
    // ...
}
Saga模式

Saga模式通常适用于业务流程长或多的场景。
这里不过多介绍,详见官网:https://seata.io/zh-cn/docs/user/saga.html

其他补充

除 AT 模式外二阶段只有成功 / 失败重试两种逻辑?

Seata设计认为二阶段应处于最终一致性状态,当不一致时理应就不断的去重试直到成功,并且二阶段并不应该考虑各种异常情况,异常应当只会有数据库连接不上这种情况。针对这种情况做法应该是监控系统及时报警然后及时恢复数据库的连接。
对于TCC模式二阶段的处理不应该有太多的复杂逻辑存在;若是程序的编码错误,不应该考虑在内。
对于AT模式由于其二阶段回滚依赖于undo_log镜像所以提供了失败异常处理。

TCC 模式异常重试机制

TCC模式中如果二阶段出现异常,则会每秒一次去不断重试(1.6.0,后续或会增加重试间隔配置),即使服务重启也会去不断重试;这是根据server端记录的事务状态去驱动执行客户端二阶段方法,在serverbranch_table中会记录此次的上下文信息,并传递执行二阶段。

示例项目代码地址

gitee:https://gitee.com/ahang-gitee/learn-seata

你可能感兴趣的:(分布式,数据库,微服务,java)