一、前言
本章学习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)适合对接外部系统的分布式长事务;