深度剖析Saga分布式事务

时间:2022-12-07 13:00:45


简介:大家好,我是枫哥????一线互联网的IT民工、????资深面试官、????Java跳蚤网课堂创始人。拥有多年一线研发经验,曾就职过科大讯飞、美团网、平安等公司。在上海有自己小伙伴组建的副业团队,目前业余时间专注Java技术分享,春招/秋招/社招/跳槽,一对一学习辅助,项目接活开发。

???? 欢迎关注 ???? 收藏 ????留言 ????

????????:文末送福利

目录

​​saga的理论来源​​

​​回滚与重试​​

​​并发执行​​

​​saga的实现分类​​

​​状态机实现​​

​​非状态机实现​​

​​saga设计dtm。​​

​​解决问题实例​​

​​高级用法​​

​​支持重试和回滚。​​

​​部分第三方操作无法回滚​​

​​超时回滚​​

​​其他分支的结果作为输入​​

​​小结​​


 

saga是分布式事务领域非常重要的事务模式,特别适合解决旅游订票等长期事务。本文将深入分析saga事务的设计原理和解决订票问题的最佳实践。

saga的理论来源

这种事务模式saga最早来自这篇论文。​​http://www.amundsen.com/downloads/sagas.pdf

深度剖析Saga分布式事务

http://www.amundsen.com/downloads/sagas.pdf​

在这篇论文中,作者提出将一个长事务分为多个子事务,每个子事务都有正向操作Ti和反向补偿操作Ci。

如果Ti依次成功完成所有子事务,则全局事务完成。

如果子事务Ti失败,将调用Ci、Ci-1、Ci-2..进行补偿。

论文阐述了上述部分的基本saga逻辑后,提出了以下场景的技术处理。

回滚与重试

如果一项SAGA事务在执行过程中失败,那么接下来有两种选择,一种是回滚,另一种是继续重试。

回滚机制比较简单,下一步的操作只需要将下一步的操作记录到保存点即可。一旦出现问题,从保存点回滚,反向执行所有补偿操作。

如果一个持续了一天的长期事务被服务器重启等临时失败中断,如果此时只能回滚,业务是不可接受的。这时候最好的策略就是在保存点重试,让事务继续,直到事务完成。
重试的支持需要提前安排和保存整体事务的所有子事务,然后在失败时重新读取未完成的进度,继续重试。

并发执行

对于长期事务来说,并发执行的特点也很重要。在并行支持下,一个串行耗时一天的长期事务可能需要半天时间才能完成,对业务帮助很大。

在某些场景下,并发执行子事务是业务的必要要求,比如订多张票,机票确认时间长的时候,不要等前一张票确认后再订票,会导致订票成功率大幅下降。

在子事务并发执行的情况下,支持回滚和重试将面临更大的挑战,涉及更复杂的保存点。

saga的实现分类

目前市面上已经实现了很多saga,都有saga的基本功能。

这些实现大致可以分为两类。

状态机实现

这种典型的seatasaga实现了,他引入了DSL语言定义状态机,允许用户做以下操作:

  • 子事务结束后,根据子事务的结果,决定下一步该怎么办。
  • 能够将子事务执行的结果保存到状态机中,并作为后续子事务的输入。
  • 允许不依赖的子事务并发执行。

这种方法的优点是:

  • 功能强大,事务可以灵活定义。

缺点是:

  • 状态机的使用门槛很高,需要了解相关DSL,可读性差,问题难以调试。官方例子是一个全局事务,包括两个子事务。Json格式状态机定义95行左右,很难入门。
  • 接口入侵强,只能使用特定的输入输出接口参数类型。在云原生时代,对强型GRPC不友好(GRPC协议,TM无法获得用户定制的输入输出pb文件,无法分析结果中的字段)

非状态机实现

有eventuate的saga,dtm的saga。

在这种实现中,没有引入新的DSL来实现状态机,而是使用函数接口来定义整体事务下的所有分支事务:

优点:

  • 易上手,易维护。

缺点:

  • 很难灵活定义状态机的事务。

PS:eventuate的作者将基于事件订阅合作模式,也称为saga。因为他的影响力很大,很多文章在介绍saga模式的时候都会提到这个。但其实这种模式和原来的saga论文关系不大,和各家实现的saga模式关系不大,所以这里没有专门讨论这种模式。

saga设计dtm。

dtm支持TCC和saga模式,它们有不同的特点,各自适应不同的业务场景,相互补充。

深度剖析Saga分布式事务

 上表对比了TCC和SAGA这两种交易模式。

TCC的定位是一致性要求高的短事务。一致性要求高的事务一般都是短事务(一个事务长期未完成,在用户眼里一致性比较差,一般不需要采用TCC的高一致性设计),所以TCC的事务分支排放在AP端(即程序代码),用户灵活调用。这样用户就可以根据每个分支的结果灵活判断和执行。

SAGA的定位是长事务/短事务,一致性要求低。对于预订机票这样的场景,持续时间长,可能持续几分钟到一两天。需要将整个事务安排保存到服务器中,避免因升级、故障等原因导致事务安排信息丢失。

状态机提供的灵活性对于客户端安排的TCC来说是不必要的,但是对于保存在服务器端的saga来说是有意义的。当我第一次设计saga时,我做了详细的权衡选择。这种状态机很难上手,用户很容易气馁。我找了一些用户做需求研究,总结出来的核心需求是:

  • 子事务并发执行,减少延迟。比如旅游订票业务预订往返机票,因为订票可能需要很长时间才能确认,等机票订好了再订返程机票,很容易导致订不到。
  • 有些操作不能回滚,需要放在可回滚子事务之后,保证一旦实施,最终会成功。

在这两个核心需求下,dtm的saga最终没有使用状态机,而是支持子事务的并发执行和指定子事务之间的顺序关系。

以实际问题为例,说明dtm中saga的用法。

对于订票业务,子事务的执行结果不是立即返还的,通常是第三方在订票后一段时间内通知结果。在这种情况下,dtm的saga提供了很好的支持,支持子事务返回的结果和指定的重试时间间隔。订票的子事务可以在自己的逻辑中。如果没有下单,就下单;如果已经下单,此时是重试请求,可以去第三方查询结果,最后返回成功/失败/进行。

解决问题实例

我们用一个真实的用户案例来讲解dtmsaga的最佳实践。

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

要求:

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

以上要求,正是saga事务模式需要解决的问题,我们来看看dtm是如何解决的(以Go语言为例)。

首先,我们根据要求1创建一个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的不同实现及其优缺点。最后,实际问题案例详细说明dtmsaga事务的使用。

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

???? ????感谢大家,坚持看完,既然选择了这条路,那就一起加油,一起学习!如果需要学习资源,实战面试资料,项目资源。关注公众号:IT枫斗者,????根据关键字领取对应的资料福利????!咨询解决问题,公众号私聊枫哥,备注来意。

????回复:java全套学习资源

????回复:面试资料

????回复:枫哥简历

????回复:程序员表白神器               

(从此告别程序员单身狗!)

????回复:程序员兼职网站

????回复:枫哥666                             

( 获取66套项目实战资料,大厂面试视频)