The SAGA transaction mode is the most commonly used mode in DTM, mainly because the SAGA mode is easy to use, requires less work, and can solve most business needs.

dtm's SAGA mode is different from Seata's SAGA in terms of design concept, the overall difficulty of use is greatly reduced, and it is very easy to use

SAGA first appeared in the paper SAGAS published by Hector Garcaa-Molrna & Kenneth Salem in 1987 . Its core idea is to split long transactions into multiple short transactions, which are coordinated by the Saga transaction coordinator. If each short transaction is successfully submitted, the global transaction will be completed normally. If a step fails, it will be executed in reverse order once Call the compensation operation.

split into subtransactions

For example, we want to carry out a business similar to bank inter-bank transfer, and transfer 30 yuan from A to B. According to the principle of Saga transactions, we divide the entire global transaction into the following services:

  • Transfer out (TransOut) service, here transfer out will be operated A-30
  • TransOutCompensate service, roll back the above transfer out operation, that is, A+30
  • Transfer into (TransIn) service, transfer will be carried out B+30
  • Transfer into compensation (TransInCompensate) service, roll back the above transfer operation, that is, B-30

The logic of the whole SAGA transaction is:

Execution transfer-out success => execution transfer-in success => global transaction complete

If an error occurs in the middle, for example, an error occurs when transferring to B, the compensation operation of the executed branch will be called, that is:

Successful execution of transfer-out => execution of transfer-in failure => execution of transfer-in compensation success => execution of transfer-out compensation success => global transaction rollback completed

Let's look at a typical sequence diagram of a successfully completed SAGA transaction:

Distributed transaction [SAGA transaction mode]_global transaction

In this figure, our global transaction initiator submits the orchestration information of the entire global transaction, including the forward operation and reverse compensation operation of each step, to the server, and the server will execute the previous SAGA step by step. logic.

Access to SAGA

Let's see how Go accesses a SAGA transaction

req := &gin.H{"amount": 30} // 微服务的请求Body
// DtmServer为DTM服务的地址
saga := dtmcli.NewSaga(DtmServer, shortuuid.New()).
  // 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCompensate"
  Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req).
  // 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransIn", 逆向操作为url: qsBusi+"/TransInCompensate"
  Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req)
// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务
err := saga.Submit()

上面的代码首先创建了一个SAGA事务,然后添加了两个子事务TransOut、TransIn,每个事务分支包括action和compensate两个操作,分别为Add函数的第一第二个参数。子事务定好之后提交给dtm。dtm收到saga提交的全局事务后,会调用所有子事务的正向操作,如果所有正向操作成功完成,那么事务成功结束。

详细例子代码参考dtm-examples

我们前面的的例子,是基于HTTP协议SDK进行DTM接入,gRPC协议的接入基本一样,详细例子代码可以在dtm-examples

失败回滚

如果有正向操作失败,例如账户余额不足或者账户被冻结,那么dtm会调用各分支的补偿操作,进行回滚,最后事务成功回滚。

我们将上述的第二个分支调用,传递参数,让他失败

Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", &TransReq{Amount: 30, TransInResult: "FAILURE"})

失败的时序图如下:

Distributed transaction [SAGA transaction mode]_state machine_02

补偿执行顺序

dtm的SAGA事务在1.10.0及之前,补偿操作是并发执行的,1.10.1之后,是根据用户指定的分支顺序,进行回滚的。

如果是普通SAGA,没有打开并发选项,那么SAGA事务的补偿分支是完全按照正向分支的反向顺序进行补偿的。

如果是并发SAGA,补偿分支也会并发执行,补偿分支的执行顺序与指定的正向分支顺序相反。假如并发SAGA指定A分支之后才能执行B,那么进行并发补偿时,DTM保证A的补偿操作在B的补偿操作之后执行

如何做补偿

当SAGA对分支A进行失败补偿时,A的正向操作可能1. 已执行;2. 未执行;3. 甚至有可能处于执行中,最终执行成功或者失败是未知的。那么对A进行补偿时,要妥善处理好这三种情况,难度很大。

dtm提供了子事务屏障技术,自动处理上述三种情况,开发人员只需要编写好针对1的补偿操作情况即可,相关工作大幅简化,详细原理,参见下面的异常章节。

失败的分支是否需要补偿

dtm 常被问到的一个问题是,TransIn返回失败,那么这个时候是否还需要调用TransIn的补偿操作?DTM 的做法是,统一进行一次调用,这种的设计考虑点如下:

  • XA, TCC 等事务模式是必须要的,SAGA 为了保持简单和统一,设计为总是调用补偿
  • DTM 支持单服务多数据源,可能出现数据源1成功,数据源2失败,这种情况下,需要确保补偿被调用,数据源1的补偿被执行
  • DTM 提供的子事务屏障,自动处理了补偿操作中的各种情况,用户只需要执行与正向操作完全相反的补偿即可

异常

在事务领域,异常是需要重点考虑的问题,例如宕机失败,进程crash都有可能导致不一致。当我们面对分布式事务,那么分布式中的异常出现更加频繁,对于异常的设计和处理,更是重中之重。

我们将异常分为以下几类:

  • 偶发失败: 在微服务领域,由于网络抖动、机器宕机、进程Crash会导致微小比例的请求失败。这类问题的解决方案是重试,第二次进行重试,就能够成功,因此微服务框架或者网关类的产品,都会支持重试,例如配置重试3次,每次间隔2s。DTM的设计对重试非常友好,应当支持幂等的各个接口都已支持幂等,不会发生因为重试导致事务bug的情况
  • 故障宕机: 大量公司内部都有复杂的多项业务,这些业务中偶尔有一两个非核心业务故障也是常态。DTM也考虑了这样的情况,在重试方面做了指数退避算法,如果遇见了故障宕机情况,那么指数退避可以避免大量请求不断发往故障应用,避免雪崩。
  • 网络乱序: 分布式系统中,网络延时是难以避免的,所以会发生一些乱序的情况,例如转账的例子中,可能发生服务器先收到撤销转账的请求,再收到转账请求。这类的问题是分布式事务中的一个重点难点问题,详情参考:异常与子事务屏障

业务上的失败与异常是需要做严格区分的,例如前面的余额不足,是业务上的失败,必须回滚,重试毫无意义。分布式事务中,有很多模式的某些阶段,要求最终成功。例如dtm的补偿操作,是要求最终成功的,只要还没成功,就会不断进行重试,直到成功。关于这部分的更详细的论述,参见最终成功

介绍到这里,您已经具备足够的知识,开发完成一个普通的SAGA任务。下面我们将介绍SAGA更加高级的知识与用法

高级用法

我们以一个真实用户案例,来讲解dtm的saga部分高级功能。

问题场景:一个用户出行旅游的应用,收到一个用户出行计划,需要预定去三亚的机票,三亚的酒店,返程的机票。

要求:

  1. 两张机票和酒店要么都预定成功,要么都回滚(酒店和航空公司提供了相关的回滚接口)
  2. 预订机票和酒店是并发的,避免串行的情况下,因为某一个预定最后确认时间晚,导致其他的预定错过时间
  3. 预定结果的确认时间可能从1分钟到1天不等

上述这些要求,正是saga事务模式擅长的领域,我们来看看dtm怎么解决。

首先我们根据要求1,创建一个saga事务,这个saga包含三个分支,分别是,预定去三亚机票,预定酒店,预定返程机票

saga := dtmcli.NewSaga(DtmServer, gid).
			Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketInfo1).
			Add(Busi+"/BookHotel", Busi+"/BookHotelRevert", bookHotelInfo2).
			Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketBackInfo3)

然后我们根据要求2,让saga并发执行(默认是顺序执行)

saga.EnableConcurrent()

最后我们处理3里面的“预定结果的确认时间”不是即时响应的问题。由于不是即时响应,所以我们不能够让预定操作等待第三方的结果,而是提交预定请求后,就立即返回状态-进行中。我们的分支事务未完成,dtm会重试我们的事务分支,我们把重试间隔指定为1分钟。

saga.RetryInterval = 60
  saga.Submit()
// ........
func bookTicket() string {
	order := loadOrder()
	if order == nil { // 尚未下单,进行第三方下单操作
		order = submitTicketOrder()
		order.save()
	}
	order.Query() // 查询第三方订单状态
	return order.Status // 成功-SUCCESS 失败-FAILURE 进行中-ONGOING
}

固定间隔重试

dtm默认情况下,重试策略是指数退避算法,可以避免出现故障时,过多的重试导致负载过高。但是这里订票结果不应当采用指数退避算法重试,否则最终用户不能及时收到通知。因此在bookTicket中,返回结果ONGOING,当dtm收到这个结果时,会采用固定间隔重试,这样能及时通知到用户。

更多高级场景

在实际应用中,还遇见过一些业务场景,需要一些额外的技巧进行处理

部分第三方操作无法回滚

例如一个订单中的发货,一旦给出了发货指令,那么涉及线下相关操作,那么很难直接回滚。对于涉及这类情况的saga如何处理呢?

我们把一个事务中的操作分为可回滚的操作,以及不可回滚的操作。那么把可回滚的操作放到前面,把不可回滚的操作放在后面执行,那么就可以解决这类问题

saga := dtmcli.NewSaga(DtmServer, shortuuid.New()).
			Add(Busi+"/CanRollback1", Busi+"/CanRollback1Revert", req).
			Add(Busi+"/CanRollback2", Busi+"/CanRollback2Revert", req).
			Add(Busi+"/UnRollback1", "", req).
			Add(Busi+"/UnRollback2", "", req).
			EnableConcurrent().
			AddBranchOrder(2, []int{0, 1}). // 指定step 2,需要在0,1完成后执行
			AddBranchOrder(3, []int{0, 1}) // 指定step 3,需要在0,1完成后执行

示例中的代码,指定Step 2,3 中的 UnRollback 操作,必须在Step 0,1 完成后执行。

对于不可回滚的操作,DTM的设计建议是,不可回滚的操作在业务上也不允许返回失败。可以这么思考,如果发货的操作返回了失败,那么这个失败的含义是不够清晰的,调用方不知道这个失败是修改了部分数据的失败,还是修改数据前的业务校验失败,因为这个操作不可回滚,所以调用方收到这个失败,是不知道如何正确处理这个错误的。

另外当你的一个全局事务中,如果出现了两个既不可回滚的又可能返回失败的操作,那么到了实际运行中,一个执行成功,一个执行失败,此时执行成功的那个事务无法回滚,那么这个事务的一致性就不可能保证了。

对于发货操作,如果可能在校验数据上可能发生失败,那么将发货操作拆分为发货校验、发货两个服务则会清晰很多,发货校验可回滚,发货不可回滚同时也不会失败。

超时回滚

saga属于长事务,因此持续的时间跨度很大,可能是100ms到1天,因此saga没有默认的超时时间。

dtm支持saga事务单独指定超时时间,到了超时时间,全局事务就会回滚。

saga.TimeoutToFail = 1800

在saga事务中,设置超时时间一定要注意,这类事务里不能够包含无法回滚的事务分支,因为超时回滚时,已执行的无法回滚的分支,数据就是错的。

其他分支的结果作为输入

前面的设计环节讲了为什么dtm没有支持这样的需求,那么如果极少数的实际业务有这样的需求怎么处理?例如B分支需要A分支的执行结果

dtm的建议做法是,在ServiceA再提供一个接口,让B可以获取到相关的数据。这种方案虽然效率稍低,但是易理解已维护,开发工作量也不会太大。

PS:有个小细节请注意,尽量在你的事务外部进行网络请求,避免事务时间跨度变长,导致并发问题。

如果您需要其他分支的结果作为输入,也可以考虑一下dtm里面的 TCC 模式,该模式有不同的适用场景,但是提供了非常便捷的获取其他分支结果的接口

SAGA 设计原则

Seata的SAGA采用了状态机实现,而DTM的SAGA没有采用状态机,因此常常有用户会问,为什么DTM没有采用状态机,状态机可以提供更加灵活的事务自定义。

我在DTM设计SAGA高级用法时,充分调研了状态机实现,经过仔细权衡之后,决定不采用状态机实现,主要原因如下:

易用性对比

可能在阿里内部,需要SAGA提供类似状态机的灵活性,但是在阿里外部,看到使用Seata的Saga事务的用户特别少。我调研了Seata中SAGA的开发资料,想要上手写一个简单的SAGA事务,需要

  1. 了解状态机的原理
  2. 上手状态机的图形界面工具,生成状态机定义Json(一个简单的分布式事务任务,需要大约90多行的Json定义)
  3. 将上述Json配置到Java项目中
  4. 如果遇见问题,需要跟踪调试状态机定义的调用关系,非常复杂

而对比之下,DTM的SAGA事务,则非常简单易用,开发者没有理解成本,通常五六行代码就完成了一个全局事务的编写,因此也成为DTM中,应用最为广泛的事务模式。而对于高级场景,DTM也经过实践的检验,以极简单的选项,例如EnableConcurrent、RetryInterval,解决了复杂的应用场景。目前收集到的用户需求中,暂未看到状态机能解决,而DTM的SAGA不能解决的案例。

gRPC友好度

gRPC 是云原生时代中应用非常广泛的协议。而Seata的状态机,对HTTP的支持度较好,而对gRPC的支持度不友好。一个gRPC服务中返回的结果,如果没有相关的pb定义文件,就无法解析出其中的字段,因此就无法采用状态机做灵活的判断,那么想用状态机的话,就必须固定结果类型,这样对应用的侵入性就比较强,适用范围就比较窄。

DTM则对gRPC的支持更加友好,对结果类型无任何要求,适用范围更加广泛。

小结

Here is a detailed introduction of SAGA from simple usage to advanced usage. If you are proficient in SAGA transactions in DTM, you can solve most of the problems in distributed transactions.