乐趣区

关于数据库:深度剖析Saga分布式事务

saga 是分布式事务畛域里一个十分重要的事务模式,特地适宜解决出行订票这类的长事务,本文将深度分析 saga 事务的设计原理,以及在解决订票问题上的最佳实际

saga 的实践起源

saga 这种事务模式最早来自这篇论文:sagas

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

如果所有的子事务 Ti 顺次胜利实现,全局事务实现

如果子事务 Ti 失败,那么会调用 Ci, Ci-1, Ci-2 …. 进行弥补

论文论述了上述这部分根本的 saga 逻辑之后,提出了上面几种场景的技术解决

回滚与重试

对于一个 SAGA 事务,如果执行过程中遭逢失败,那么接下来有两种抉择,一种是进行回滚,另一种是重试持续。

回滚的机制绝对简略一些,只须要在进行下一步之前,把下一步的操作记录到保留点就能够了。一旦呈现问题,那么从保留点处开始回滚,反向执行所有的弥补操作即可。

如果有一个继续了一天的长事务,被服务器重启这类长期失败中断后,此时如果只能进行回滚,那么业务是难以承受的。此时最好的策略是在保留点处重试并让事务持续,直到事务实现。

往前重试的反对,须要把全局事务的所有子事务当时编排好并保留,而后在失败时,从新读取未实现的进度,并重试继续执行。

并发执行

对于长事务而言,并发执行的个性也是至关重要的,一个串行耗时一天的长事务,在并行的反对下,可能半天就实现了,这对业务的帮忙很大。

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

在子事务并发执行的场景下,反对回滚与重试,挑战会更大,波及了较简单的保留点。

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 模式,这两个模式有不同的特点,各自适应不同的业务场景,互相补充。

上述这张表,很好的比拟了 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。

退出移动版