Seata源码(五)Saga模式

426 阅读22分钟

一、前言

本章学习seata(1.5.0) saga模式原理。

saga模式的介绍见论文和官网,笔者就不班门弄斧了,复制粘贴也没什么意思,讲的还不如人家清楚。

saga模式在网上有很多文章,偏向于理论,但是工业级的实现较少,幸好能看到seata对于saga模式的实现。

本文按照笔者的思路将seata saga模式拆解,主要是为其他看源码的朋友提供个思路,没耐心的读者可以直接跳过看总结能理解个大概。

二、使用案例

本小节基于github.com/seata/seata…的案例,移植到springboot中。

需要注意的是,之前无论是tcc模式还是at模式,参与全局事务的每个分支事务所在服务,都需要引入seata依赖并进行相应的代码修改。

使用saga模式,只需要TM服务进行修改,比如之前案例中的business-service。

本次案例就使用business-service作为TM,开启全局事务,所以只需要对business-service进行改造,实现扣减库存+扣减积分,针对于扣减库存和扣减积分都提供了相应的回滚方法。

1、DDL

1)状态机描述

CREATE TABLE IF NOT EXISTS `business_seata_state_machine_def`
(
    `id`               VARCHAR(32)  NOT NULL COMMENT 'id',
    `name`             VARCHAR(128) NOT NULL COMMENT 'name',
    `tenant_id`        VARCHAR(32)  NOT NULL COMMENT 'tenant id',
    `app_name`         VARCHAR(32)  NOT NULL COMMENT 'application name',
    `type`             VARCHAR(20)  COMMENT 'state language type',
    `comment_`         VARCHAR(255) COMMENT 'comment',
    `ver`              VARCHAR(16)  NOT NULL COMMENT 'version',
    `gmt_create`       DATETIME(3)  NOT NULL COMMENT 'create time',
    `status`           VARCHAR(2)   NOT NULL COMMENT 'status(AC:active|IN:inactive)',
    `content`          TEXT COMMENT 'content',
    `recover_strategy` VARCHAR(16) COMMENT 'transaction recover strategy(compensate|retry)',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

2)状态机实例

每开启一个saga事务,business_seata_state_machine_inst就会多一条数据,代表状态机实例,注意business_key和tenant_id构成的唯一约束。

CREATE TABLE IF NOT EXISTS `business_seata_state_machine_inst`
(
    `id`                  VARCHAR(128)            NOT NULL COMMENT 'id',
    `machine_id`          VARCHAR(32)             NOT NULL COMMENT 'state machine definition id',
    `tenant_id`           VARCHAR(32)             NOT NULL COMMENT 'tenant id',
    `parent_id`           VARCHAR(128) COMMENT 'parent id',
    `gmt_started`         DATETIME(3)             NOT NULL COMMENT 'start time',
    `business_key`        VARCHAR(48) COMMENT 'business key',
    `start_params`        TEXT COMMENT 'start parameters',
    `gmt_end`             DATETIME(3) COMMENT 'end time',
    `excep`               BLOB COMMENT 'exception',
    `end_params`          TEXT COMMENT 'end parameters',
    `status`              VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `compensation_status` VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `is_running`          TINYINT(1) COMMENT 'is running(0 no|1 yes)',
    `gmt_updated`         DATETIME(3) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `unikey_buz_tenant` (`business_key`, `tenant_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

3)状态机实例状态

每个saga事务会经历多个状态,每个状态对应business_seata_state_inst一条数据。

CREATE TABLE IF NOT EXISTS `business_seata_state_inst`
(
    `id`                       VARCHAR(48)  NOT NULL COMMENT 'id',
    `machine_inst_id`          VARCHAR(128) NOT NULL COMMENT 'state machine instance id',
    `name`                     VARCHAR(128) NOT NULL COMMENT 'state name',
    `type`                     VARCHAR(20)  COMMENT 'state type',
    `service_name`             VARCHAR(128) COMMENT 'service name',
    `service_method`           VARCHAR(128) COMMENT 'method name',
    `service_type`             VARCHAR(16) COMMENT 'service type',
    `business_key`             VARCHAR(48) COMMENT 'business key',
    `state_id_compensated_for` VARCHAR(50) COMMENT 'state compensated for',
    `state_id_retried_for`     VARCHAR(50) COMMENT 'state retried for',
    `gmt_started`              DATETIME(3)  NOT NULL COMMENT 'start time',
    `is_for_update`            TINYINT(1) COMMENT 'is service for update',
    `input_params`             TEXT COMMENT 'input parameters',
    `output_params`            TEXT COMMENT 'output parameters',
    `status`                   VARCHAR(2)   NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `excep`                    BLOB COMMENT 'exception',
    `gmt_updated`              DATETIME(3) COMMENT 'update time',
    `gmt_end`                  DATETIME(3) COMMENT 'end time',
    PRIMARY KEY (`id`, `machine_inst_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

2、编写状态机描述json文件

这个描述文件比较复杂,难以理解,通过后续的源码分析,具体看一下其中每个参数的含义,对整体事务流转会产生什么影响。

{
  "Name": "reduceInventoryAndBalance", // 状态机名称
  "Comment": "reduce inventory then reduce balance in a transaction",
  "StartState": "ReduceInventory", // 从哪个状态开始
  "Version": "0.0.1",
  "States": { // 状态map,key是状态名称,value是状态描述
    "ReduceInventory": { // 扣减库存服务
      "Type": "ServiceTask", // 类型
      "ServiceName": "inventoryAction", // beanName
      "ServiceMethod": "reduce", // 方法名
      "CompensateState": "CompensateReduceInventory", // 补偿状态
      "Next": "ChoiceState", // 下一个状态
      "Input": [ // 参数列表
        "$.[businessKey]",
        "$.[count]"
      ],
      "Output": { // 出参
        "reduceInventoryResult": "$.#root"
      },
      "Status": { // 执行状态 SU-成功、FA-失败、UN-未知
        "#root == true": "SU",
        "#root == false": "FA",
        "$Exception{java.lang.Throwable}": "UN"
      }
    },
    "ChoiceState": { // 可以通过根据不同条件,路由到不同的状态
      "Type": "Choice",
      "Choices": [
        {
          "Expression": "[reduceInventoryResult] == true",
          "Next": "ReduceBalance" // 如果扣减库存成功,路由到扣减积分
        }
      ],
      "Default": "Fail"
    },
    "ReduceBalance": { // 扣减积分服务
      "Type": "ServiceTask",
      "ServiceName": "balanceAction", // beanName
      "ServiceMethod": "reduce", // 方法名
      "CompensateState": "CompensateReduceBalance", // 补偿状态
      "Input": [ // 入参列表
        "$.[businessKey]",
        "$.[amount]",
        {
          "throwException": "$.[mockReduceBalanceFail]"
        }
      ],
      "Output": { // 出参
        "compensateReduceBalanceResult": "$.#root"
      },
      "Status": { // 执行状态 SU-成功、FA-失败、UN-未知
        "#root == true": "SU",
        "#root == false": "FA",
        "$Exception{java.lang.Throwable}": "UN"
      },
      "Catch": [ // 自定义异常处理
        {
          "Exceptions": [
            "java.lang.Throwable"
          ],
          "Next": "CompensationTrigger" // 发生任何异常,进入CompensationTrigger
        }
      ],
      "Next": "Succeed"
    },
    "CompensateReduceInventory": { // 扣减库存回滚服务
      "Type": "ServiceTask",
      "ServiceName": "inventoryAction",
      "ServiceMethod": "compensateReduce",
      "Input": [
        "$.[businessKey]"
      ]
    },
    "CompensateReduceBalance": { // 扣减积分回滚服务
      "Type": "ServiceTask",
      "ServiceName": "balanceAction",
      "ServiceMethod": "compensateReduce",
      "Input": [
        "$.[businessKey]"
      ]
    },
    "CompensationTrigger": {
      "Type": "CompensationTrigger", // 反向补偿
      "Next": "Fail" // 补偿完成后的状态
    },
    "Succeed": {
      "Type": "Succeed"
    },
    "Fail": {
      "Type": "Fail",
      "ErrorCode": "PURCHASE_FAILED",
      "Message": "purchase failed"
    }
  }
}

3、application.properties

seata.saga.enabled=true:开启saga模式的自动配置(SeataSagaAutoConfiguration);

seata.saga.state-machine.table-prefix:设置saga相关表前缀,默认是seata_;

seata.saga.state-machine.resources:设置状态机描述文件扫描路径,默认是classpath*:seata/saga/statelang/**/*.json;

seata.enabled=true
seata.tx-service-group=business-service
seata.service.vgroup-mapping.business-service=default
## Seata注册中心使用file
seata.registry.type=file
seata.service.grouplist.default=127.0.0.1:8091
## saga
seata.saga.enabled=true
seata.saga.state-machine.table-prefix=business_seata_
seata.saga.state-machine.resources=classpath:/seata/saga/*.json

4、业务代码

1)dubbo

@Configuration
public class DubboConfiguration {
	// 库存服务
    @Bean
    public ReferenceBean<InventoryAction> inventoryAction() {
        ReferenceBean<InventoryAction> bean = new ReferenceBean<>();
        bean.setInterface(InventoryAction.class);
        return bean;
    }
	// 积分服务
    @Bean
    public ReferenceBean<BalanceAction> balanceAction() {
        ReferenceBean<BalanceAction> bean = new ReferenceBean<>();
        bean.setInterface(BalanceAction.class);
        return bean;
    }
}
public interface InventoryAction {

    boolean reduce(String businessKey, int count);

    boolean compensateReduce(String businessKey);
}
public interface BalanceAction {

    boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params);

    boolean compensateReduce(String businessKey, Map<String, Object> params);
}

2)入口

@RestController
public class SagaController {

	// saga自动配置状态机引擎,用于执行saga事务
    @Autowired
    private StateMachineEngine stateMachineEngine;

    @GetMapping(value = "/seata/saga/commit", produces = "application/json")
    public String commit() {
        transactionCommittedDemo(stateMachineEngine);
        return "ok";
    }
    private static void transactionCommittedDemo(StateMachineEngine stateMachineEngine) {
    	// 参数列表
        Map<String, Object> startParams = new HashMap<>(3);
        // 业务唯一键,比如订单号
        String businessKey = String.valueOf(System.currentTimeMillis());
        startParams.put("businessKey", businessKey);
        startParams.put("count", 10);
        startParams.put("amount", new BigDecimal("100"));
        // 执行状态机引擎
        // 参数1:状态机名称
        // 参数2:tenantId多租户支持,数据隔离
        // 参数3:businessKey 业务唯一键
        // 参数4:参数列表,用于json描述文件中Input使用
        StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("reduceInventoryAndBalance", null, businessKey, startParams);
        Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());
        System.out.println("saga transaction commit succeed. XID: " + inst.getId());
    }
}

三、从自动配置入手

SeataSagaAutoConfiguration注册了saga模式需要的所有bean,默认情况下需要设置seata.saga.enabled=true才能开启。

1、StateMachineConfig

StateMachineConfig主要管理状态机引擎的所有配置,实现类是DbStateMachineConfig。

接入springboot后,通过ConfigurationProperties注入seata.saga.state-machine配置。

1)关于datasource:saga使用的datasource可以和业务datasource分开,需要用户自己定义seataSagaDataSource名称的datasource;这也与at模式和tcc模式不太一样,建议最好还是分开,避免saga与业务相互影响(比如业务连接池爆了导致saga也无法正常执行);

2)异步saga事务:需要用户开启seata.saga.state-machine.enable-async=true,默认注入seataSagaAsyncThreadPoolExecutor;

2、StateMachineEngine

StateMachineEngine是暴露给用户的api入口,依赖StateMachineConfig配置信息,实现类是ProcessCtrlStateMachineEngine。

StateMachineEngineHolder提供静态方法给框架内部随时获取StateMachineEngine实例,不知道为什么这里setStateMachineEngine方法不做成静态的,比较奇怪。

3、异步支持

注意,在1.5.0版本中,开启异步支持会报错,原因是SagaAsyncThreadPoolProperties会重复注册(ComponentScan+EnableConfigurationProperties),导致ioc容器中存在两个SagaAsyncThreadPoolProperties类型bean,sagaAsyncThreadPoolExecutor方法注入失败。

可能到1.6.0才能修复(github.com/seata/seata…)。

如果非要用,自己创建同名bean(sagaAsyncThreadPoolExecutor)覆盖即可。

四、客户端初始化

seata将组件的初始化工作基本都放在了bean的afterPropertiesSet初始化阶段,在这个阶段期望了解下面几个问题:

1)json状态机描述文件和seata_state_machine_def表的关系

2)状态机模型是怎样的

3)什么会被作为Resource向TC注册(at模式datasource、tcc模式TwoPhaseBusinessAction、saga模式呢)

1、DbStateMachineConfig

由于StateMachineEngine依赖StateMachineConfig,我们先看StateMachineConfig的初始化。

DbStateMachineConfig初始化拆分为5个关键步骤:

1)获取db类型,底层根据connection.getMetaData().getDatabaseProductName()获取,这里是MySQL;

2)创建StateLogStore,注入StateMachineConfig,目前也仅有db实现类DbAndReportTcStateLogStore。

这个StateLogStore负责运状态机实例的存储,对于db存储来说,就是seata_state_machine_inst状态机实例表(StateMachineInstance)、seata_state_inst状态机实例状态表(StateInstance)。

3)创建SagaTransactionalTemplate,注入StateLogStore

SagaTransactionalTemplate和GlobalTransactional注解驱动的全局事务模板TransactionalTemplate的功能非常相似,主要是管理全局事务的开始、提交、回滚。

主要区别在于,SagaTransactionalTemplate还包含了RM能力,比如branchRegister分支事务注册。

所以可以认为SagaTransactionalTemplate接口包含saga事务中TM和RM角色的所有能力。

这里我们需要关注一下DefaultSagaTransactionalTemplate的初始化操作。

除了常规的初始化TM和RM客户端以外,SagaResource被作为资源注册到TC,applicationId+txServiceGroup作为资源id,即在saga模式下一个应用是一个Resource。(AT模式下是DataSource、TCC模式下是TwoPhaseBusinessAction)

4)创建StateLangStore,注入StateMachineConfig

StateLangStore负责存储和查询状态机的定义,目前仅有DbStateLangStore实现。

StateMachine模型对应seata_state_machine_def表(也对应我们定义的json文件,之后会看到)。

StateMachine模型如下,与我们json文件的定义几乎一致。

每个StateMachine包含多个states定义,State是状态顶层抽象接口。

比如ServiceTask类型的State模型如下,主要用于反射调用业务方法。

2、DefaultStateMachineConfig

DbStateMachineConfig初始化完成后,执行父类DefaultStateMachineConfig的初始化方法。

DefaultStateMachineConfig初始化过程中创建了很多组件:

1)ExpressionFactoryManager/EvaluatorFactoryManager:前者创建Expression表达式,后者Evaluator解析Expression表达式为true or false;

2)StateMachineRepository:注入StateLangStore,提供StateMachine定义的增删改查能力,把StateLangStore包一层,StateLangStore可以在子类进行替换;

3)stateMachineRepository.registryByResources:将外部Resource注册到StateMachineRepository,这里将我们写的json描述文件持久化到了db,并缓存到内存中;目前stateMechine的定义不支持热更新,需要通过registryByResources这个API自己实现,可以看到registryByResources API始终以传入Resource的状态机为准

4)StateLogRepository:和StateMachineRepository类似,注入StateLogStore,提供运行时状态机实例的增删改查能力,StateLogStore可以在子类进行替换;

5)StatusDecisionStrategy:根据状态机执行结果,设置执行状态和补偿状态;

6)syncProcessCtrlEventPublisher/asyncEventPublisher:同步和异步事件发布器,用于实现saga事务同步和异步执行;

7)ServiceInvokerManager:负责执行ServiceTask,目前仅支持springbean的方式;

五、客户端运行时

1)saga模式有没有分支事务的概念?什么会作为分支事务?是serviceTask吗?

2)异常场景如何处理?如何决定是向前重试还是向后补偿?

3)怎么决定下一个执行哪个state?

4)超时时间由谁来控制?TC还是TM?

我们可以借助官方提供的seata-test模块来分析,这边提供了多种多样的测试用例,无需外部组件依赖就可以完成整个源码流程debug。

1、主方法startInternal

StateMachineEngine.start底层就是调用startInternal方法。

主方法一共分为5步:

1)查询状态机配置,构建状态机实例

ProcessCtrlStateMachineEngine#createMachineInstance

由于在初始化阶段已经将json文件存储到db和内存中,这里只需要从内存获取状态机配置即可。

再组装一个新的StateMachineInstanceImpl状态机实例。

2)构建上下文ProcessContext

ProcessContextImpl上下文仅有三个属性:

variables:map存储所有东西;

instruction:一个标记接口,目前只有基于状态机的实现StateInstruction,代表当前执行的指令;

parent:支持父子Context,比如getVariable会先从本地variable获取,找不到再从parent获取;

3)StateLogStore记录状态机实例启动(重点)

4)执行(重点)

5)返回状态机实例

对于简单的一笔略过,接下来展开分析3和4两个步骤。

2、状态机启动

DbAndReportTcStateLogStore#recordStateMachineStarted启动状态机实例一共分为两步:

1)请求tc开启全局事务;

2)持久化StateMachineInstance;

开启全局事务最终调用SagaTransactionalTemplate.beginTransaction,传入状态机配置中的TransOperationTimeout作为超时时间(默认30分钟)。

SagaTransactionalTemplate.beginTransaction底层还是构建GlobalTransaction,调用GlobalTransaction.begin向tc发起GlobalBeginRequest,tc返回xid,tm将xid绑定在GlobalTransaction实例上。

3、执行

无论是同步还是异步执行,都走ProcessCtrlEventPublisher,但是底层EventBus不同。

异步EventBus=AsyncEventBus。将Context上下文异步提交到处理类处理。

同步EventBus=DirectEventBus。这个也是重点分析的EventBus。

当context首次进入EventBus,会创建一个Stack,存储这个context,并执行EventConsumer。

当context二次进入EventBus,只会将context再次入栈,不会执行EventConsumer。

然后首次进入EventBus会循环到currentStack.size大于0,继续处理context。

这个逻辑很绕,但是这和后面ProcessContext每次的路由息息相关,在每个state执行完后,重新路由后都会再次publish到EventBus,不理解的可以继续看后面。

对于ProcessContext目前只有一个EventConsumer,即ProcessCtrlEventConsumer。

ProcessCtrlEventConsumer委派给ProcessControllerImpl处理。

所有的业务处理,都分为StateMachineProcessHandler#process处理阶段和DefaultRouterHandler#route路由阶段。

接下来再拆分为process和route来具体分析。

4、process阶段

StateMachineProcessHandler根据上下文中的StateInstruction,选择不同的StateHandler处理状态。

对于不同的StateHandler可能还有不同的前置拦截和后置拦截逻辑。

接下来根据不同的state类型,分析不同的handler和interceptor逻辑,主要分析:ServiceTask、Choice、CompensationTrigger。

ServiceTask

"SecondState": {
    "Type": "ServiceTask",
    "ServiceName": "demoService",
    "ServiceMethod": "randomExceptionMethod",
    "Retry": [
        {
            "Exceptions": ["io.seata.saga.engine.mock.DemoException"],
            "IntervalSeconds": 1.5,
            "MaxAttempts": 3,
            "BackoffRate": 1.5
        }
    ],
    "Catch": [
        {
            "Exceptions": [
                "io.seata.saga.engine.mock.DemoException"
            ],
            "Next": "Fail"
        }
    ],
    "Next": "Succeed"
}

ServiceTaskHandlerInterceptor.preProcess

ServiceTask类型state先要经过ServiceTaskHandlerInterceptor拦截。

ServiceTaskHandlerInterceptor.preProcess主要做了三个事情,这三步有异常发生,都会通过EngineUtils.failStateMachine+抛出异常中断整个流程:

1)事务超时检测,只要stateMachineInstance的更新时间不超时即可

2)构建StateInstanceImpl状态实例,解析入参,放入上下文

3)注册分支事务(默认关闭),并持久化StateInstanceImpl

DbAndReportTcStateLogStore#recordStateStarted先开启分支事务,然后持久化StateInstance。

但是,默认情况下sagaBranchRegisterEnable=false,在saga模式下rm角色不会向tc发起BranchRegisterRequest, 基本可以肯定saga模式下,分支事务的概念基本不存在。

ServiceTaskStateHandler

ServiceTask的实际执行阶段就个关键点:

1)执行SpringBeanServiceInvoker,调用目标方法;

2)如果执行异常,进入EngineUtils.handleException,但是不会主动抛出异常;

执行目标方法

SpringBeanServiceInvoker区分同步或异步执行state(注意这里的异步,是state定义时用户设置为异步,并非用户stateMachineEngine.startAsync api异步执行状态机的异步),但是异步执行state并不会返回任何异常(除了线程池爆了,但是线程池等待队列是无界的Integer.MAX_VALUE),那异步state异常如何进行状态流转呢?(存疑)

SpringBeanServiceInvoker#doInvoke,反射调用目标方法,并支持重试逻辑。

这里重试比较特别的是,state可以存在多种重试策略,对于每个重试策略有一个计数器,比如下面这个配置,最多能重试6次。

"state1": {
    // ...
    "Retry": [
        {
            "Exceptions": ["io.seata.saga.engine.mock.DemoException"],
            "IntervalSeconds": 1.5,
            "MaxAttempts": 3,
            "BackoffRate": 1.5
        },
        {
            "IntervalSeconds": 1,
            "MaxAttempts": 3,
            "BackoffRate": 1.5
        }
    ],
    //...
}

获取重试策略的方法如下:

1)根据配置策略,从前往后依次遍历匹配;

2)如果没有配置异常对应重试策略,则仅判断当前发生的异常是否是网络异常;

3)如果配置异常对应重试策略,则判断异常是否匹配;

上述分析对应官网描述

Retry: 捕获异常后的重试策略, 是个数组可以配置多个规则, Exceptions 为匹配的的异常列表, IntervalSeconds 为重试间隔, MaxAttempts 为最大重试次数, BackoffRate 下一次重试间隔相对于上一次重试间隔的倍数,比如说上次一重试间隔是2秒, BackoffRate=1.5 则下一次重试间隔是3秒。Exceptions 属性可以不配置, 不配置时表示框架自动匹配网络超时异常。当在重试过程中发生了别的异常,框架会重新匹配规则,并按新规则进行重试,同一种规则的总重试次数不会超过该规则的MaxAttempts

异常处理

如果业务代码发生异常,EngineUtils#handleException根据用户定义的catch匹配到异常后,将next存入上下文,如果没匹配到,将一个boolean标志位放入上下文。

比如下面这个状态机,如果匹配到DemoException,会将CompensationTrigger放入上下文(VAR_NAME_CURRENT_EXCEPTION_ROUTE),影响之后的路由决策。

{
    //...
    "States": {
        "FirstState": {
            "Type": "ServiceTask",
            "ServiceName": "demoService",
            "ServiceMethod": "foo",
            // ...
            "Status": {
                "$Exception{io.seata.saga.engine.mock.DemoException}": "UN",
                "#root != null": "SU",
                "#root == null": "FA"
            },
            "Catch": [
                {
                    "Exceptions": [
                        "io.seata.saga.engine.mock.DemoException"
                    ],
                    "Next": "CompensationTrigger"
                }
            ]
        }
    }
}

ServiceTaskHandlerInterceptor.postProcess

ServiceTaskHandlerInterceptor#postProcess后处理,分为四个步骤:

1)根据执行情况,决定State执行状态executionStatus(重点)

2)解析出参,放入上下文

3)StateInstance结束(重点)

4)如果异常没有被用户定义catch处理,StateMachineInstance结束(重点)

决策State执行状态

ServiceTaskHandlerInterceptor#decideExecutionStatus:StateInstance的执行状态处理,在preProcess中,StateInstance的状态被置为RU,即running正在运行,在postProcess中进行状态更新。

{
  //...
  "Status": {
    "$Exception{io.seata.saga.engine.mock.DemoException}": "UN",
    "#root != null": "SU",
    "#root == null": "FA"
  }
}

首先根据用户定义的Status决定执行状态:

1)如果是异步state,则跳过该步骤

2)在未发生异常的情况下,如果用户定义status没有mapping到状态,最终StateInstance会以Unknown结束(isForUpdate=true,代表数据更新服务),整个状态机StateMachineInstance会以异常结束,并抛出异常到用户代码。 所以未发生异常,如果用户自定义状态Status,必须要定义一个兜底的映射;

3)在发生异常的情况下,如果用户定义status没有mapping到状态,走saga默认逻辑;

如果用户没有定义Status或发生异常未匹配Status,那么走默认逻辑:

1)无异常,Success;(异步state一定进入success,也就是说异步state对整个状态流程几乎没影响,只要提交到线程池成功,就是成功)

2)非数据更新服务,发生异常,结果为Fail;

3)数据更新服务,发生异常,根据异常类型决定:

3-1)如果异常是无法建立网络连接ConnectionException,状态是Fail

3-2)其他异常(包括业务异常、网络超时)状态都是Unknown

State执行完成持久化

DbAndReportTcStateLogStore#recordStateFinished:

1)持久化StateInstance,将出参和异常,还有最终状态,都持久化到db;

2)因为默认没开启sagaBranchRegisterEnable,如果state执行失败,也不会发送branchReport给tc汇报分支事务状态;

State执行异常,状态机以失败结束

如果State定义的catch没有对发生的异常做特殊处理(比如触发CompensationTrigger回滚补偿,或进入其他状态),那么状态机实例将以失败结束,注意当前StateInstance的状态已经被决策,不是FA就是UN。这个放在第6点总结。

Choice

"ChoiceState":{
    "Type": "Choice",
    "Choices":[
        {
            "Expression":"[a] == 1",
            "Next":"SecondState"
        },
        {
            "Expression":"[a] == 2",
            "Next":"ThirdState"
        }
    ],
    "Default":"SecondState"
}

先无视route路由,看一下ChoiceState的实现。

ChoiceState没有拦截逻辑,直接进入process方法。

优先匹配Choices中的每个expression。由于使用LinkedHashMap,所以按照定义的顺序,从上到下优先匹配。

如果没有匹配到Choices,采用Default返回,如果没有定义Default,状态机实例异常结束,抛出异常到用户。

ChoiceState将匹配到的状态,放入上下文,给route路由提供支持。

CompensationTrigger

如果要触发saga二阶段补偿回滚,必须手动catch异常,通过next指向CompensationTrigger

{
    "States": {
        // ...
        "SecondState": {
            "Type": "ServiceTask",
            "ServiceName": "demoService",
            "ServiceMethod": "bar",
            "Catch": [
                {
                    "Exceptions": [
                        "io.seata.saga.engine.mock.DemoException"
                    ],
                    "Next": "CompensationTrigger"
                }
            ],
            "Next": "Succeed"
        },
        "CompensationTrigger": {
            "Type": "CompensationTrigger",
            "Next": "Fail"
        },
        "Fail": {
            "Type":"Fail",
            "ErrorCode": "NOT_FOUND",
            "Message": "not found"
        }
    }
}

CompensationTriggerStateHandler开启二阶段回滚流程。

1)找出所有需要被补偿的StateInstance

2)在上下文中创建一个栈,将需要被补偿的StateInstance入栈

3)标记状态机status=Unknown,compensationStatus=Running

4)在上下文中VAR_NAME_CURRENT_COMPEN_TRIGGER_STATE放入当前补偿状态CompensationTrigger,为后续route路由做铺垫

CompensationHolder#findStateInstListToBeCompensated从StateInstance中选择需要补偿的状态,对于ServiceTask,Status非Fail && CompensationStatus非Success && 有补偿状态的StateInstance都会加入补偿状态集合。

结合上面StateInstance的状态决策,比如发生网络连接异常,状态为Fail,就不会加入补偿状态集合。

Succeed/Fail

用户可以自己定义业务流程,最终转向Succeed代表业务成功,或Fail代表业务失败。

"Succeed": {
    "Type":"Succeed"
},
"Fail": {
    "Type":"Fail",
    "ErrorCode": "NOT_FOUND",
    "Message": "not found"
}

Succeed目前没有做任何处理。

FailEndStateHandler在上下文中放入了一些变量,比如配置的errorcode,没做特殊处理。

5、route阶段

DefaultRouterHandler处理route整体逻辑,通过ProcessRouter处理返回下一个要执行的StateInstruction,将上下文重新通过EventPublisher发布。

对于DirectEventBus会将Context写入Context.VAR_NAME_SYNC_EXE_STACK栈即结束。

执行阶段currentStack.size>0,会从栈中捞出来路由后的Context,继续执行下一个State。

public class DirectEventBus extends AbstractEventBus<ProcessContext> {
    private static final String VAR_NAME_SYNC_EXE_STACK = "_sync_execution_stack_";
    @Override
    public boolean offer(ProcessContext context) throws FrameworkException {
        boolean isFirstEvent = false;
        Stack<ProcessContext> currentStack = (Stack<ProcessContext>)context.getVariable(VAR_NAME_SYNC_EXE_STACK);
        if (currentStack == null) { // 不会进入这里,因为第一个state执行已经初始化了栈
            isFirstEvent = true;
            // 初始化Stack
        }
        currentStack.push(context); // 入栈
        if (isFirstEvent) { // false,路由后不会重复执行
            try {
                while (currentStack.size() > 0) {
                    ProcessContext currentContext = currentStack.pop();
                    for (EventConsumer eventHandler : eventHandlers) {
                        eventHandler.process(currentContext);
                    }
                }
            } finally {
                context.removeVariable(VAR_NAME_SYNC_EXE_STACK);
            }
        }
        return true;
    }
}

ProcessRouter就一个实现TaskStateRouter根据state的执行情况,路由到下一个状态,注意返回的Instruction实例没变,只是修改了stateName而已。

一般有四种场景:

1)instruction已经结束;

2)CompensationTrigger设置补偿路由VAR_NAME_CURRENT_COMPEN_TRIGGER_STATE;

3)state.Catch设置exception路由VAR_NAME_CURRENT_EXCEPTION_ROUTE;

4)Choice设置路由VAR_NAME_CURRENT_CHOICE;

TaskStateRouter#compensateRoute根据补偿执行情况路由。

case1:如果发生异常,或StateInstance执行非成功,结束状态机endStateMachine;

case2:从StateInstance栈中弹出下一个要补偿的State,路由到对应compensateState;

case3:StateInstance栈为空,代表补偿流程已经走完,路由到CompensationTrigger.next,如果next不存在结束状态机endStateMachine;

6、状态机结束

状态机结束其实在process和route阶段都存在,多种情况下都会引发状态机结束,主要分为状态机执行失败、状态机执行成功。

状态机执行失败

状态机执行失败(EngineUtils#failStateMachine),触发条件有很多,根据上面的流程举例来说:

1)ServiceTaskHandlerInterceptor#preProcess:检测事务超时StateMachineExecutionTimeout;

2)ServiceTaskHandlerInterceptor#preProcess:入参解析失败VariablesAssignError;

3)ServiceTaskHandlerInterceptor#preProcess:StateInstance持久化失败ExceptionCaught;

4)ServiceTaskHandlerInterceptor#postProcess:出参解析失败VariablesAssignError;

5)ServiceTaskHandlerInterceptor#postProcess:ServiceTask业务exception未被用户定义catch路由(EngineUtils#handleException);(重点)

6)ServiceTaskHandlerInterceptor#decideExecutionStatus:决策ServiceTask执行状态,用户定义了Status映射,在未发生异常的情况下,没有映射到Status状态NoMatchedStatus;

7)ChoiceStateHandler#process:choices匹配不到,且没有配置default,StateMachineNoChoiceMatched;

以上可以总结为三种:

1)用户配置状态机错误,比如choices、status配置映射不到;

2)框架无法处理,不可恢复错误,比如入参解析失败;

3)ServiceTask执行异常,未被catch路由,需要seata执行正向或逆向补偿;(重点)

EngineUtils#failStateMachine分为三步:

1)decideOnTaskStateFail决策失败状态(重点);

2)recordStateMachineFinished状态机结束(重点);

3)如果是异步执行状态机,执行回调函数AsyncCallback;

recordStateMachineFinished

为什么这里先看第二步,再看第一步,因为第一步的代码实在太恶心了,但是又非常关键,至于为什么关键,只有看了第二步才能看明白。

DbAndReportTcStateLogStore#recordStateMachineFinished分为三步:

1)StateMachineInstance持久化db

2)向tc汇报全局事务状态(重点)

DbAndReportTcStateLogStore#reportTransactionFinished根据StateMachineInstance的执行状态,映射为全局事务的状态,向tc汇报GlobalReportRequest,GlobalReportRequest目前只有saga模式下会存在,也很特殊。映射关系比较清楚,可以猜测对于需要重试的状态(CommitRetrying、RollbackRetrying、UnKnown)tc肯定会定时发起重试。

这里看到reportTransaction调用tc如果失败,异常都被简单捕获了,这可能导致tc会在超时之后主动发起rollback。

底层DefaultGlobalTransaction也没有做任何重试处理,不像传统tm的commit/rollback都会默认重试5次。

之所以没做重试,可能是因为tc没做幂等控制。

如果tc发起提交重试或回滚重试,tc会收到多个GlobalReport。

decideOnTaskStateFail

这个代码逻辑是在是太绕了,但是却很关键,有多复杂你可以看一下这个ISSUE(github.com/seata/seata…),一个bug两个人要来回讨论几个回合,就因为if/else写的不够严谨。

这一步我们重点关注ServiceTask业务exception未被用户定义catch路由这个场景下引发的状态机结束。

DefaultStatusDecisionStrategy#decideOnTaskStateFail根据情况选择推进还是补偿。

DefaultStatusDecisionStrategy#decideMachineForwardExecutionStatus这个方法是个公用方法,在状态机执行成功的情况下也可能会调用,暂时只关注状态机执行失败逻辑。

如果StateMachineInstance处于Running状态,代表状态机处于正向流程中,返回true;

如果StateMachineInstance处于其他状态(比如CompensationTriggerStateHandler触发,导致进入逆向补偿流程,StateMachineInstance.Status=Unknown),返回false,导致CompensationStatus=Unknown,最终会导致汇报给tc全局事务状态为RollbackRetrying二阶段回滚重试;

DefaultStatusDecisionStrategy#setMachineStatusBasedOnStateListAndException优先根据StateInstance的状态设置MachineStateInstance的状态(言外之意,可以通过state的status映射,来决定整个状态机是否向前推进),主要是三种情况:

1)存在StateInstance=Unknown,则StateMachineInstance=Unknown,比如有个State发生业务异常、超时异常,汇报CommitRetrying;

2)仅存在StateInstance=Fail和Success数据更新服务两类,则StateMachineInstance=Unknown,比如StateA成功,StateB发生网络连接异常,汇报CommitRetrying;

3)如果所有StateInstance都是Fail且没有Success数据更新服务,比如第一个State就执行失败了,汇报Finished;

最终兜底根据异常类型决定状态:

1)无异常,Success;

2)客户端超时检测异常,Unknown;

3)有异常,且有执行成功的ServiceTask,Unknown;---对应ISSUE-3284的意思是,假设数据更新stateA执行成功,数据更新stateB在ServiceTaskHandlerInterceptor#preProcess的StateInstance持久化阶段抛出异常(此时还没放到StateMachineInstance,所以根据StateInstance来决策状态没匹配,因为StateA是成功的),导致向tc汇报为Fail,无法向前推进,期望向tc汇报一阶段提交重试;

4)网络连接异常、其他异常,Fail;

5)网络超时异常,Unknown;

这里的3、4、5主要针对的是框架内部异常,因为如果是ServiceTask发生的异常,在ServiceTaskHandlerInterceptor#decideExecutionStatus阶段就已经决定了StateInstance的最终状态,在DefaultStatusDecisionStrategy#setMachineStatusBasedOnStateListAndException就可以决定StateMachineInstance的状态。

状态机执行成功

所谓状态机执行成功,就是按照用户定义的状态机流转结束,无论最终是是Fail还是Succeed。可能是正向直接成功,也可能是正向+正向补偿最终成功,也可能是正向失败触发逆向补偿最终成功。

EngineUtils#endStateMachine基本与EngineUtils#failStateMachine一致,区别在于StateMachineInstance的状态决策。

逆向状态决策

DefaultStatusDecisionStrategy#decideOnEndState:

如果已经进入补偿流程,decideMachineCompensateStatus决策CompensateStatus;

否则在正向流程,decideMachineForwardExecutionStatus决策ExecutionStatus。

DefaultStatusDecisionStrategy#decideMachineCompensateStatus决策补偿状态。

case1:如果还有需要补偿的状态,判断已经执行的补偿state的状态:

如果有成功或者未知状态的补偿state,结果为Unknown,给tc汇报RollbackRetrying;

如果所有补偿state都失败,结果为Fail,给tc汇报RollbackRetrying;

无论如何都会向tc汇报RollbackRetrying。

case2:如果补偿状态都执行完毕,判断已经执行的补偿state的状态:

如果有没成功的补偿state,结果Unknown,给tc汇报RollbackRetrying;

如果补偿state全都成功了,结果Success,给tc汇报Rollbacked。

正向状态决策

DefaultStatusDecisionStrategy#decideMachineForwardExecutionStatus在状态机执行失败里已经分析过了,决策正向状态,优先根据所有StateInstance状态决策,兜底根据异常情况决策。

但是需要关注的是specialPolicy=true这个方法块,如果最终用户自己走向了Fail状态(不是通过逆向补偿),根据StateInstance没有决策,没有异常,导致执行状态为Success,在有数据更新服务时,也会被设置为Unknown,向tc汇报Unknown导致向前补偿

public boolean decideMachineForwardExecutionStatus(StateMachineInstance stateMachineInstance, Exception exp,
                                                   boolean specialPolicy) {
    // ...
    // 根据StateInstance没有决策,没有异常,导致执行状态为Success
    setMachineStatusBasedOnStateListAndException(stateMachineInstance, stateList, exp);
    // specialPolicy = true 状态机被设置为成功,做特殊处理
    if (specialPolicy && ExecutionStatus.SU.equals(stateMachineInstance.getStatus())) {
        for (StateInstance stateInstance : stateMachineInstance.getStateList()) {
            // 如果存在一个数据更新服务,设置为Unknown,向tc汇报Unknown
            if (!stateInstance.isIgnoreStatus() && (stateInstance.isForUpdate() || stateInstance
                .isForCompensation())) {
                stateMachineInstance.setStatus(ExecutionStatus.UN);
                break;
            }
        }
        // 不存在数据更新服务,设置为Fail,向tc汇报finished
        if (ExecutionStatus.SU.equals(stateMachineInstance.getStatus())) {
            stateMachineInstance.setStatus(ExecutionStatus.FA);
        }
    }
    // ...
}

比如下面这个状态机定义,如果FirstState没有发生异常,但是被主动Choice路由到Fail,导致specialPolicy=true,会向tc汇报Unknown,导致向前推进(这只是为了解释上面的代码作用,没有实际意义,实际业务中,很多方法不会直接抛出异常,需要根据出参,比如response.errorcode,直接路由到Fail,实现向前推进状态机);这里如果将IsForUpdate改为false,则不存在数据更新服务,则直接向tc汇报Finished:

{
    "Name": "testFail",
    "StartState": "FirstState",
    "Version": "0.0.1",
    "States": {
        "FirstState": {
            "Type": "ServiceTask",
            "ServiceName": "myDemoService",
            "ServiceMethod": "foo",
            "IsForUpdate": true,
            "Next": "ChoiceState"
        },
        "ChoiceState":{
            "Type": "Choice",
            "Choices":[
                {
                    "Expression":"[a] == 1",
                    "Next":"Fail"
                },
                {
                    "Expression":"[a] == 2",
                    "Next":"Succeed"
                }
            ],
            "Default":"Succeed"
        },
        "Succeed": {
            "Type":"Succeed"
        },
        "Fail": {
            "Type":"Fail",
            "ErrorCode": "NOT_FOUND",
            "Message": "not found"
        }
    }
}

六、服务端

1、开启全局事务

关于开启全局事务在第三章AT模式的TC中介绍过,主要是将全局事务持久化。

DeafultCore#begin:

2、汇报全局事务

Saga模式下,TM通过GlobalReportRequest向TC汇报全局事务状态(TCC和AT是发送GlobalCommitRequest和GlobalRollbackRequest)。

SagaCore#doGlobalReport:

这里所有方法之前都遇到过:

case1:Committed,removeAllBranch删除分支事务(默认saga模式不会注册分支事务,可以忽略),endCommitted删除全局事务;

case2:Rollbacked/Finished,removeAllBranch删除分支事务(默认saga模式不会注册分支事务,可以忽略),endRollbacked删除全局事务;

case3:RollbackRetrying/UnKnown,queueToRetryRollback将全局事务状态更新,异步捞出来请求客户端执行向后补偿状态;(这里注意到,如果客户端检测事务超时,汇报是Unknown,tc会执行rollback重试)

case4:CommitRetrying,queueToRetryCommit将全局事务状态更新,异步捞出来请求客户端执行向前推进状态;

3、超时检测

在AT模式TC中介绍过TC全局事务超时,TC每秒检测begin状态的全局事务,对超时事务更新为TimeoutRollbacking,交给另外一个定时任务执行二阶段回滚。

DefaultCoordinator#timeoutCheck:

这个超时时间是以tc收到GlobalBeginRequest的时间为基准做校验,timeout为客户端配置的TransOperationTimeout(默认30分钟)。

4、提交重试

TC每秒查询CommitRetrying状态全局事务进行重试。

DefaultCoordinator#handleRetryCommitting:

底层saga模式的全局事务提交,走的是BranchCommitRequest。之前看到了,saga模式下默认没有rm分支事务注册逻辑,所以这里造了个假的分支事务。tc根据客户端返回状态,做全局事务状态更新。

SagaCore#doGlobalCommit:

5、回滚重试

回滚重试和提交重试类似,走分支事务回滚请求。

DefaultCoordinator#handleRetryRollbacking:

SagaCore#doGlobalRollback:

七、客户端重试

1、提交重试

客户端收到BranchCommitRequest,底层走StateMachineEngine的forward api。

SagaResourceManager#branchCommit:

底层走ProcessCtrlStateMachineEngine#forwardInternal重新从db加载出StateMachineInstance,找到向前推进的起始State并开始执行。

2、回滚重试

客户端收到BranchRollbackRequest,底层走StateMachineEngine的compensate api。

值得注意的是,如果配置状态机的RecoverStrategy为forward,可以在超时后不回滚,仍然尝试向前恢复,驱使tc走commit逻辑。

SagaResourceManager#branchRollback:

ProcessCtrlStateMachineEngine#compensateInternal和提交重试逻辑类似,值得注意的是,StateInstruction被设置了TemporaryState(CompensationTrigger),这是为了复用CompensationTriggerStateHandler向前回滚补偿的逻辑,从正向StateInstance中挑选出需要被回滚补偿的State,CompensationTriggerStateHandler也会是第一个被执行的State。

在process阶段,StateInstruction#getState会优先返回临时状态。

public State getState(ProcessContext context) {
    if (getTemporaryState() != null) {
        return temporaryState;
    }
    // ...
    return state;
}

在route阶段,优先取TemporaryState作为当前state做判断,并在路由后直接移除。

public class StateMachineProcessRouter implements ProcessRouter {
    @Override
    public Instruction route(ProcessContext context) throws FrameworkException {
        StateInstruction stateInstruction = context.getInstruction(StateInstruction.class);
        State state;
        if (stateInstruction.getTemporaryState() != null) {
            state = stateInstruction.getTemporaryState();
            stateInstruction.setTemporaryState(null);
        }
        // ...
    }
}

3、获取重试结果

无论正向推进还是逆向补偿,都是异步的,在start api调用后如何获取重试结果。

一种方式,根据状态机实例id从db加载StateMachineInstance,判断执行状态和补偿状态。

@Test
public void test() throws Exception {
    StateMachineInstance inst = stateMachineEngine.start(stateMachineName, tenantId, paramMap);
    while (!(ExecutionStatus.SU.equals(inst.getStatus()) || ExecutionStatus.SU.equals(inst.getCompensationStatus()))) {
        inst = stateMachineEngine.getStateMachineConfig().getStateLogStore().getStateMachineInstance(inst.getId());
    }
}

如果业务场景拿不到状态机实例id,可以通过businessKey业务唯一键加载StateMachineInstance。

@Test
public void test() throws Exception {
    stateMachineEngine.startWithBusinessKey(stateMachineName, tenantId, businessKey, paramMap);
    // ...
    StateMachineInstance inst = null;
	do {
        inst = stateMachineEngine.getStateMachineConfig().getStateLogStore().getStateMachineInstanceByBusinessKey(businessKey,tenantId);
    } while (inst != null && (!(ExecutionStatus.SU.equals(inst.getStatus()) || ExecutionStatus.SU.equals(inst.getCompensationStatus()))));
}

八、总结

模型

状态机定义被抽象为StateMachine,每个StateMachine包含多个State状态定义。

这些定义被用户写在json文件中,在spring启动过程中被持久化到db。

State状态有多种实现,type不同实现不同。

ServiceTaskState:业务方法,可以设置其对应补偿状态CompensateState做回滚,可以设置catch、retry、status、async等高级用法;

ChoiceState:选择,可以设置choices根据SpringEL表达式,选择下一个next状态,在业务中往往针对ServiceTask出参路由到不同的ServiceTask/CompensationTrigger;

CompensationTriggerState:反向补偿触发器,一般通过ServiceTask的catch异常next指向触发,根据已执行的状态列表,推算需要反向补偿的状态;

在运行时,每开启一个saga事务创建一个状态机实例StateMachineInstance,StateMachineInstance每执行一个ServiceTask类型的State,会关联一个状态实例StateInstance。注意只有ServiceTask类型的State会创建StateInstance,包含正向ServiceTask和逆向ServiceTask。

整体流程

客户端初始化阶段(DbStateMachineConfig#afterPropertiesSet):

1)从json文件加载StateMachine到db(seata_state_machine_def);

2)TM和RM客户端初始化,与TC建立连接;

3)SagaResource被作为资源注册到TC,applicationId+txServiceGroup作为资源id,即在saga模式下一个应用是一个Resource;(AT模式下是DataSource、TCC模式下是TwoPhaseBusinessAction)

客户端执行StateMachineEngine#start指定状态机名称,设置入参,开启一个saga事务。

在saga事务中,默认没有分支事务注册,所以可以认为只有TM角色,没有RM角色。

整个流程中只有GlobalBeginRequest开启全局事务和GlobalReportRequest汇报全局事务两个tm到tc的调用。

tc根据汇报的状态不同,先更新db,然后采取不同措施:

汇报事务状态措施
Finished全局事务结束
Committed全局事务结束
Rollbacked全局事务结束
RollbackRetrying异步重试回滚(逆向推进状态)
UnKnown异步重试回滚(逆向推进状态)
CommitRetrying异步重试提交(正向推进状态)

对于提交重试,tc向tm发送BranchCommitRequest,tm走StateMachineEngine#forward api从db加载出StateMachineInstance,找到向前推进起始State,正向推进状态机。

对于回滚重试,tc向tm发送BranchRollbackRequest,tm走StateMachineEngine#compensate api,复用CompensationTriggerStateHandler逆向补偿的逻辑,从正向StateInstance中挑选出需要被回滚补偿的State,逆向推进状态机。

ServiceTaskState

每个ServiceTaskState业务方法状态,都会在执行前创建StateInstance。

StateInstance状态决策,决定了StateMachineInstance的状态,也决定了向tc汇报的全局事务状态。

首先根据用户定义的Status决定执行状态:

1)如果是异步state,则跳过该步骤;

2)未发生异常,如果status没匹配,抛出异常到用户代码;

3) 发生异常,如果status没匹配,走默认逻辑;

默认状态决策:

1)无异常,Success;

2)非数据更新服务,发生异常,结果为Fail;

3)数据更新服务,发生异常,根据异常类型决定:

  • 无法建立网络连接ConnectionException,状态Fail;
  • 其他异常(包括业务异常、网络超时)状态Unknown;

状态机状态决策

考虑正常状态机执行结束的情况。

如果StateMachineInstance的CompensationStatus处于RU状态,代表处于逆向流程,决策逆向状态CompensationStatus(DefaultStatusDecisionStrategy#decideMachineCompensateStatus):

1)如果补偿State未执行完毕,且存在补偿StateUN或SU,则UN;---RollbackRetrying

2)如果补偿State未执行完毕,且补偿State都FA,则FA;---RollbackRetrying

3)如果补偿State执行完毕,且存在补偿State非SU,则UN;---RollbackRetrying

4)如果补偿State执行完毕,且补偿State都SU,则SU;---Rollbacked

如果StateMachineInstance的CompensationStatus处于非RU状态,代表处于正向流程,决策正向状态Status(DefaultStatusDecisionStrategy#decideMachineForwardExecutionStatus):

1)如果存在UN状态StateInstance,则UN;---CommitRetrying

2)如果存在FA状态StateInstance,且存在SU状态数据更新State,则UN;---CommitRetrying

3)如果存在FA状态StateInstance,且不存在SU状态数据更新State,则FA;---Finished

4)如果没有异常,则SU;---Committed

关于全局事务超时

全局事务的超时时间默认为30分钟,可以通过在客户端设置TransOperationTimeout来改变。

对于tm来说,会在每次执行serviceTask前做超时时间,比较StateMachineInstance的更新时间和TransOperationTimeout,如果超时,会向tc汇报全局事务状态Unknown,而tc会执行回滚,向tm发出BranchRollbackRequest。

对于tc来说,会比较全局事务开始时间和TransOperationTimeout,如果超时会主动向tm发出BranchRollbackRequest。

本质上是tc在控制超时,因为tm超时是根据更新时间来的,这意味着如果不停重试,tm基本不可能超时,而tc是根据全局事务开始时间来的,这个时间不可变。

在tm收到tc的回滚请求后,会比较恢复策略RecoverStrategy是否是forward:

如果是forward,会响应tc为PhaseTwo_CommitFailed_Retryable,tc会将这个全局事务设置为CommitRetrying,执行向前推进;

默认情况下tm会执行compensate api进行向后回滚。

综上所述,全局事务的超时在tc测和tm测都有检测,一般情况下发现超时会执行compensate api回滚,但是可以通过RecoverStrategy=forward干预,实现forward向前推进。

与AT和TCC比较

隔离性

AT模式有全局锁,可以避免脏读脏写;

TCC模式三个方法一般有业务含义,比如账户类服务:额度冻结Try、额度扣减Confirm、额度解冻Cancel;

SAGA模式隔离性差,官方也有说明和举例(seata.io/zh-cn/docs/…)。

角色

AT和TCC模式中都有明显的RM角色,负责注册分支事务和提交/回滚分支事务。

在SAGA模式中,默认没有分支事务注册(sagaBranchRegisterEnable=false),客户端只有一个TM角色,负责开启全局事务和汇报全局事务。

虽然没有BranchRegister,但是如果tm汇报CommitRetrying或RollbackRetrying给tc,tc会发起BranchCommit和BranchRollback,对应forward和compensate api。

混合模式全局事务

在框架层面上,AT和TCC可以在一个全局事务中混合使用,而SAGA全局事务和其他模式不共存。

优势

1)可以只在TM侧做代码改动;

2)只需要提供正向和逆向两个方法;

3)无锁;

4)适合对接外部系统的分布式长事务;