深度剖析Saga分布式事务

2021年11月25日 阅读数:15
这篇文章主要向大家介绍深度剖析Saga分布式事务,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

saga是分布式事务领域里一个很是重要的事务模式,特别适合解决出行订票这类的长事务,本文将深度剖析saga事务的设计原理,以及在解决订票问题上的最佳实践算法

saga的理论来源

saga这种事务模式最先来自这篇论文:sagassegmentfault

在这篇论文里,做者提出了将一个长事务,分拆成多个子事务,每一个子事务有正向操做Ti,反向补偿操做Ci。服务器

假如全部的子事务Ti依次成功完成,全局事务完成网络

假如子事务Ti失败,那么会调用Ci, Ci-1, Ci-2 ....进行补偿并发

论文阐述了上述这部分基本的saga逻辑以后,提出了下面几种场景的技术处理分布式

回滚与重试

对于一个SAGA事务,若是执行过程当中遭遇失败,那么接下来有两种选择,一种是进行回滚,另外一种是重试继续。函数

回滚的机制相对简单一些,只须要在进行下一步以前,把下一步的操做记录到保存点就能够了。一旦出现问题,那么从保存点处开始回滚,反向执行全部的补偿操做便可。学习

假若有一个持续了一天的长事务,被服务器重启这类临时失败中断后,此时若是只能进行回滚,那么业务是难以接受的。 此时最好的策略是在保存点处重试并让事务继续,直到事务完成。ui

往前重试的支持,须要把全局事务的全部子事务事先编排好并保存,而后在失败时,从新读取未完成的进度,并重试继续执行。spa

并发执行

对于长事务而言,并发执行的特性也是相当重要的,一个串行耗时一天的长事务,在并行的支持下,可能半天就完成了,这对业务的帮助很大。

某些场景下并发执行子事务,是业务必须的要求,例如订多张及票,而机票确认时间较长时,不该当等前一个票已经确认以后,再去定下一张票,这样会致使订票成功率大幅降低。

在子事务并发执行的场景下,支持回滚与重试,挑战会更大,涉及了较复杂的保存点。

saga的实现分类

目前看到市面上已经有不少的saga实现,他们都具有saga的基本功能。

这些实现,能够大体能够分为两类

状态机实现

这一类的典型实现有seata的saga,他引入了一个DSL语言定义的状态机,容许用户作如下操做:

  • 在某一个子事务结束后,根据这个子事务的结果,决定下一步作什么
  • 可以把子事务执行的结果保存到状态机,并在后续的子事务中做为输入
  • 容许没有依赖的子事务之间并发执行

这种方式的优势是:

  • 功能强大,事务能够灵活自定义

缺点是:

  • 状态机的使用门槛很是高,须要了解相关DSL,可读性差,出问题难调试。官方例子是一个包含两个子事务的全局事务,Json格式的状态机定义大约有95行,较难入门。
  • 接口入侵强,只能使用特定的输入输出接口参数类型,在云原生时代,对强类型的gRPC不友好

非状态机实现

这一类的实现有eventuate的saga,dtm的saga。

在这一类的实现中,没有引入新的DSL来实现状态机,而是采用函数接口的方式,定义全局事务下的各个分支事务:

优势:

  • 简单易上手,易维护

缺点:

  • 难以作到状态机的事务灵活自定义

PS:eventuate的做者将基于事件订阅协做的模式,也称为saga,由于他的影响力大,所以许多文章在介绍saga模式的时候都会提这个。但事实上这个模式与原先的saga论文相关不大,也与各家实现的saga模式相关不大,因此这里没有专门去论述这种模式

还有许多其余的saga实现,例如servicecomb-pack,Camel,hmily.因为精力有限,没有一一研究。后续作了更多研究后,会继续更新文章

dtm的saga设计

dtm支持TCC和saga模式,这两个模式有不一样的特色,各自适应不一样的业务场景,相互补充。

image.png

上述这张表,很好的比较了TCC和SAGA这两种事务模式。

TCC的定位是一致性要求较高的短事务。一致性要求较高的事务通常都是短事务(一个事务长时间未完成,在用户看来一致性是比较差的,通常没有必要采用TCC这种高一致性的设计),所以TCC的事务分支编排放在了AP端(即程序代码里),由用户灵活调用。这样用户能够根据每一个分支的结果,作灵活的判断与执行。

SAGA的定位是一致性要求较低的长事务/短事务。对于相似订机票这种这样的场景,持续时间长,可能持续几分钟到一两天,就须要把整个事务的编排保存到服务器,避免发起全局事务的APP由于升级、故障等缘由,致使事务编排信息丢失。

状态机提供的灵活性对于在客户端编排的TCC是不必的,可是对于保存在服务器端的saga是有意义的。我在最初设计saga的时候,进行了较详细的权衡取舍。状态机的这种方式,上手难度很是高,用户容易望而却步。我找了一些用户作需求调研,总结出来的核心需求有:

  • 子事务并发执行,下降延时。例如旅游订票业务的预约往返机票,由于订票可能须要较长时间才可以确认,等去的机票定好以后再订返程票,容易致使订不上。
  • 有些操做没法回滚,须要放在可回滚的子事务以后,保证一旦执行,就可以最终成功。

在这两项核心需求下,dtm的saga最终没有采用状态机,可是支持了子事务的并发执行以及指定子事务之间的顺序关系。

下面咱们以一个实际问题做为例子,讲解dtm中saga的用法

对于订票类业务,子事务的执行结果不是当即返回的,一般是预约机票后,过一段时间第三方才通知结果。对于这种状况dtm的saga提供了良好的支持,它支持子事务返回进行中的结果,并支持指定重试时间间隔。订票的子事务能够在本身的逻辑中,若是未下订单,则下订单;若是已下订单,那么此时就是重试的请求,能够去第三方查询结果,最后返回成功/失败/进行中。

解决问题实例

咱们以一个真实用户案例,来说解dtm的saga最佳实践。

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

要求:

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

上述这些要求,正是saga事务模式要解决的问题,咱们来看看dtm怎么解决(以Go语言为例)。

首先咱们根据要求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.SetOptions(&dtmcli.TransOptions{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要求业务明确返回如下几个值:

  • SUCCESS表示分支成功,能够进行下一步
  • FAILURE 表示分支失败,全局事务失败,须要回滚
  • ONGOING表示进行中,后续按照正常的间隔进行重试
  • 其余表示系统问题,后续按照指数退避算法进行重试

部分第三方操做没法回滚

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

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

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

超时回滚

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

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

    saga.SetOptions(&dtmcli.TransOptions{TimeoutToFail: 1800})

在saga事务中,设置超时时间必定要注意,这类事务里不可以包含没法回滚的事务分支,不然超时回滚这类的分支会有问题。

其余分支的结果做为输入

若是极少数的实际业务不只须要知道某些事务分支是否执行成功,还想要得到成功的详细结果数据,那么dtm如何处理这样的需求呢?例如B分支须要A分支的执行成功返回的详细数据。

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

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

小结

本文总结了saga相关的理论知识、设计原则,对比了saga的不一样实现及其优缺点。最后以一个现实中的问题案例,详细讲解dtm的saga事务使用

dtm是一个一站式的分布式事务解决方案,支持事务消息、SAGA、TCC、XA等多种事务模式,支持Go、Java、Python、PHP、C#、Node等语言SDK。

项目文档还详细讲解了分布式事务相关的基础知识、设计理念和最新理论,是学习分布式事务的绝佳资料。

欢迎你们访问yedf/dtm,给咱们Issue、PR、Star。