原理-官网

角色

TC-事务协调者

  • seata自己独立部署的一个server
  • 维护全局和分支事务的状态,驱动全局事务提交或回滚

    TM-事务管理器

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

    RM-资源管理器

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

    AT模式

    环境搭建

    部署seata-server

    下载 seat-server

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

    设置seata-server的注册中心、配置中心

  • 编辑 conf/registry.conf

    • 修改type为nacos

      初始化seata配置

  • 下载初始化脚本:https://github.com/seata/seata/blob/1.4.2/script/config-center/nacos/nacos-config.sh

  • 下载初始化配置文件:https://github.com/seata/seata/blob/1.4.2/script/config-center/config.txt
  • 修改配置
    • store.mode=db
    • 修改数据库配置信息
    • 修改redis配置信息
  • 执行初始化脚本,它会读取config.txt中的配置
    • 修改脚本中的nacos地址信息
  • 在nacos控制台配置中心页面,可以看到SEATA_GROUP分组的配置

    初始化seata全局事务数据库

  • 创建seata数据库,使用如下脚本初始化数据库

  • https://github.com/seata/seata/blob/1.4.2/script/server/db/mysql.sql
  • 会创建如下三张表

    • global_table 保存全局事务数据;
    • branch_table 保存分支事务数据;
    • lock_table 保存锁定资源数据。

      启动seata-server

  • sh bin/seata-server.sh

  • 输出如下信息表示启动成功,nacos服务列表也会有seats-server这个服务

    1. 16:19:04.000 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /Users/zxl/soft/seata/seata-server-1.4.2/conf/registry.conf
    2. 16:19:06.724 INFO --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
    3. 16:19:07.318 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091

    对应服务数据库创建undo_log表

  • 下载sql脚本:https://github.com/seata/seata/blob/1.4.2/script/client/at/db/mysql.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';
    

    AT模式

    项目部分

    配置文件

    #seata配置
    seata:
    #  开启seata分布式事务
    enabled: true
    #  事务服务分组名,与naocs一致
    tx-service-group: my_test_tx_group
    #  是否启用数据源代理
    enable-auto-data-source-proxy: true
    #  事务服务配置
    service:
      vgroup-mapping:
    #      事务分组对应集群名称
        my_test_tx_group: default
      grouplist:
    #      Seata-Server服务的IP地址与端口
        default: 192.168.31.107:8091
      enable-degrade: false
      disable-global-transaction: false
    #    Nacos配置中心信息
    config:
      type: nacos
      nacos:
        namespace:
        serverAddr: 192.168.31.10:8848
        group: SEATA_GROUP
        username: nacos
        password: nacos
        cluster: default
    #      Nacos注册中心信息
    registry:
      type: nacos
      nacos:
        application: seata-server
        server-addr: 192.168.31.10:8848
        group : SEATA_GROUP
        namespace:
        username: nacos
        password: nacos
        cluster: default
    

    数据源代理类

  • 用于生成反向SQL

    @Configuration
    public class DataSourceProxyConfig {
      //创建Druid数据源
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource")
      public DruidDataSource druidDataSource() {
          return new DruidDataSource();
      }
      //建立DataSource数据源代理
      @Primary
      @Bean
      public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
          return new DataSourceProxy(druidDataSource);
      }
    }
    

    GlobalTransactional

    执行流程

    伪代码

    会员采购(){
      订单服务.创建订单();
      积分服务.增加积分();
      库存服务.减少库存();
    }
    

    流程

    seata执行流程.png

    两阶段

  • 一阶段:执行核心业务逻辑(即代码中的 CRUD 操作)。Seata 会根据 DB 操作自动生成相应的回滚日志,并将回滚日志添加到 RM 对应的 undo_log 表中。执行业务代码和添加回滚日志这两步都是在同一个本地事务中提交的。

  • 二阶段:如果全局事务的最终决议是 Commit,则更新分支事务状态并清空回滚日志;如果最终决议是 Rollback,则根据 undo_log 中的回滚日志进行 rollback 操作。二阶段是以异步化的方式来执行的

    优点-执行效率高

  • 一是核心业务逻辑可以在一阶段得到快速提交,DB 资源被快速释放;

  • 二是全局事务的 Commit 和 Rollback 是异步执行

    流程

    image.png

    第一阶段�

  • Customer 服务作为分布式事务的起点,扮演了一个 TM 的角色,它会向 TC 注册并发起一个全局事务。全局事务会生成一个 XID,它是全局唯一的 ID 标识,所有分支事务都会和这个 XID 进行绑定

  • XID 在服务内部(非跨服务调用)的传播机制是基于 ThreadLocal 构建的,即 XID 在当前线程的上下文中进行透传,对于跨服务调用来说,则依赖 seata-all 组件内置的各个适配器(如 Interceptor 和 Filter)将 XID 传递给对象服务。
  • 然后,Customer 服务调用了 Template 服务进行模板注销流程,Template 服务的 RM 开启了一个分支事务,并注册到 TC。在执行分支事务的过程中,RM 还会生成回滚日志并提交到 undo_log 表中。除此之外,RM 还需要获取到两个特殊的 Lock。其中一个是 Local Lock(本地锁),另一个是 Global Lock(全局锁)
  • Lock 信息存放在 lock_table 这张表里,它会记录待修改的资源 ID 以及它的全局事务和分支事务 ID 等信息。无论是一阶段提交还是二阶段回滚,RM 都需要获取待修改记录的本地锁,然后才会去执行 CRUD 操作
  • 而在 RM 提交一阶段事务之前,它还会尝试获取 Global Lock(全局锁),目的是防止多个分布式事务对同一条记录进行修改。假设有两个不同的分布式事务想要修改记录 A,那么只有同时获取到 Local Lock 和 Global Lock 的事务才能正常提交一阶段事务。
  • 本地锁会随一阶段事务的提交 / 回滚而释放,而全局锁只有等到全局事务提交 / 回滚之后才会被释放
  • 在一阶段中,如果某一个事务在一定的尝试次数后仍然无法获取全局锁,它会知难而退,执行本地事务回滚操作
  • 而如果在二阶段回滚的时候,RM 无法获取本地锁,它会原地打转不停重试,直到成功获取本地锁并完成重试。

    二阶段

  • 当所有分支事务commit/rollback后,开启二阶段

  • TC 向各个分支下达了二阶段决议。如果最终决议是 Commit,那么各个 RM 会执行一段异步操作,删除 undo_log;如果最终决议是 Rollback,那么 RM 端会根据 undo_log 中记录的回滚日志做反向补偿。

    使用

    ```java

com.alibaba.cloud spring-cloud-starter-alibaba-seata

<a name="dWg3m"></a>
#### 数据源

- 为了能够在分支事务开启 / 提交等关键节点上做一番手脚(比如向 Seata 注册分支事务、生成 undo_log 等),我们需要用 Seata 特有的数据源“接管”项目原有的数据源。
```java

@Configuration
public class SeataConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Bean("dataSource")
    @Primary
    public DataSource dataSourceDelegation(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }

}

配置


spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: seata-server-group

seata:
  application-id: coupon-customer-serv
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: localhost:8848
      namespace: dev
      group: myGroup
      cluster: default
  service:
    vgroup-mapping:
      seata-server-group: default

编码

  • 开启分布式事务 ```java

@DeleteMapping(“template”) @GlobalTransactional(name = “coupon-customer-serv”, rollbackFor = Exception.class) public void deleteCoupon(@RequestParam(“templateId”) Long templateId) { customerService.deleteCouponTemplate(templateId); }


- 回滚日志
   - rm handle branch rollback process:本地资源管理器开始执行回滚流程。
   - Branch Rollbacking:分支事务正在回滚。
   - Branch Rollbacked result: PhaseTwo_Rollbacked:分支事务回滚完成
<a name="oaSbP"></a>
### 
<a name="E41Pg"></a>
### 问题
<a name="Q4Bf4"></a>
#### 并不所有的服务都依赖或使用关系行数据库
<a name="b5QKa"></a>
#### 全局事务并发更新

- 解决方法:select ... for update
- <br />
<a name="DvSNB"></a>
#### 全局事务外的更新

- @GlobalLock
- 竞争全局锁
<a name="kU9q6"></a>
#### SQL限制

- 不支持 SQL 嵌套
- 不支持多表复杂 SQL
- 不支持存储过程、触发器
- 不支持批量更新 SQL
<a name="K9PLz"></a>
## TCC模式
<a name="bnYrH"></a>
### TCC 事务模型

- Try 阶段完成的工作是预定操作资源(Prepare),说白了就是“占座”的意思,在正式开始执行业务逻辑之前,先把要操作的资源占上座。
- Confirm 阶段完成的工作是执行主要业务逻辑(Commit),它类似于事务的 Commit 操作。在这个阶段中,你可以对 Try 阶段锁定的资源进行各种 CRUD 操作。如果 Confirm 阶段被成功执行,就宣告当前分支事务提交成功。
- Cancel 阶段的工作是事务回滚(Rollback),它类似于事务的 Rollback 操作。在这个阶段中,你可没有 AT 方案的 undo_log 帮你做自动回滚,你需要通过业务代码,对 Confirm 阶段执行的操作进行人工回滚。
<a name="YUhuD"></a>
### 使用

- @LocalTCC 注解被用来修饰实现了二阶段提交的本地 TCC 接口,而 @TwoPhaseBusinessAction 注解标识当前方法使用 TCC 模式管理事务提交
- 在 TCC 模式下,查询参数将作为 BusinessActionContext 的一部分,在事务上下文中进行传递。
- 源码:从 GlobalTransactionScanner 这个类的 wrapIfNecessary 方法开始研究。它通过 TCCBeanParserUtils 工具类来判断当前资源是否为 TCC 的实现类,如果是 TCC 自动代理的话,就生成一个 TccActionInterceptor 作为当前 bean 对象的事务拦截器。
```java
// Try 阶段所要执行的方法,便是被 @TwoPhaseBusinessAction 所修饰的 deleteTemplateTCC 方法了。
@LocalTCC
public interface CouponTemplateServiceTCC extends CouponTemplateService {

    @TwoPhaseBusinessAction(
            name = "deleteTemplateTCC",
            commitMethod = "deleteTemplateCommit",
            rollbackMethod = "deleteTemplateCancel"
    )
    void deleteTemplateTCC(@BusinessActionContextParameter(paramName = "id") Long id);

    void deleteTemplateCommit(BusinessActionContext context);

    void deleteTemplateCancel(BusinessActionContext context);
}

空回滚

  • 所谓空回滚,是在没有执行 Try 方法的情况下,TC 下发了回滚指令并执行了 Cancel 逻辑。
  • 引入独立的事务控制表,在 Try 阶段中将 XID 和分支事务 ID 落表保存,如果 Cancel 阶段查不到事务控制记录,那么就说明 Try 阶段未被执行。同理,Cancel 阶段执行成功后,也可以在事务控制表中记录回滚状态,这样做是为了防止另一个 TCC 的坑,“倒悬”。

    原因

  • 比如某个分支事务的一阶段 Try 方法因为网络不可用发生了 Timeout 异常,或者 Try 阶段执行失败,这时候 TM 端会判定全局事务回滚,TC 端向各个分支事务发送 Cancel 指令,这就产生了一次空回滚

    解决

  • 保存全局事务xid和分支事务branchId,try阶段会插入一条记录,表示try阶段执行了。cancel方法读取该记录,如果记录存在,正常回滚;如果该记录不存在,那就是空回滚。

    幂等问题

    原因

  • 幂等是指在commit/cancel阶段,因为TC没有收到分支事务的响应,需要进行重试,这就要分支事务支持幂等

    解决

  • 可以记录一张事务控制表,保存全局事务xid和分支事务branchId,以及分支事务状态,在第二阶段commit/cancel之前先检查分支事务状态是否已经是终态,如果不是,再执行第二阶段的逻辑。

    TCC 倒悬

  • 是指 TCC 三个阶段没有按照先后顺序执行,一直处于try阶段

    原因

  • 如果 Try 方法因为网络问题卡在了网关层,导致锁定资源超时,这时 Cancel 阶段执行了一次空回滚,到目前为止一切正常。但回滚之后,原先超时的 Try 方法经过网关层的重试,又被后台服务接收到了,这就产生了一次倒悬场景,即一阶段 Try 在二阶段回滚之后被触发

  • 在倒悬的情况下,整个事务已经被全局回滚,那么如果你再执行一次 Try 操作,当前资源将被长期锁定,这就造成了一种类似死锁的局面。解法很简单,你可以利用事务控制表记录二阶段执行状态,并在 Try 阶段中检查该状态,如果二阶段回滚完毕,那么就直接跳过一阶段 Try。

    解决

  • 解决这个问题的方法就是在cancel方法中记录xid对应的分支事务回滚记录,try阶段执行的时候先判断分支事务是否已经回滚,如果存在回滚记录,则直接退出。

    TCC优化

    异步提交

  • 优化思路是try阶段成功后,不立即执行confirm/cancel阶段,而是等系统空闲的时候异步执行

    同库模式

  • TM开启全局事务时,不再需要向TC注册分支事务,而是把分支事务状态保存在了本地。TM向TC发送提交或回滚消息时,TC保存全局事务的状态。而RM则启动异步线程检测本地记录的未提交分支事务,向TC发送RPC消息获取整体事务状态,以决定是提交还是回滚本地事务。可见,优化后的模型,RPC次数减少了50%,性能大幅提升。

    Seata1.5.1:useTCCFence

  • 在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。@TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false

  • cc_fence_log 建表语句如下

    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 使用了代理数据源,使 tcc_fence_log 表操作和 RM 业务操作在同一个本地事务中执行,这样就能保证本地操作和对 tcc_fence_log 的操作同时成功或失败。

    Saga模式

  • Saga 模式适用于长流程的业务场景,用状态机来控制整个事务的执行。它使用状态图定义服务调用流程并生成 Json 状态语言定义文件,状态图的节点可以是一个服务,也可以是补偿节点

    XA模式

  • XA 模式需要分支事务数据库支持 XA 原语