saga是分布式事务领域非常重要的事务模式,特别适合解决旅游订票等长期事务。本文将深入分析saga事务的设计原理和解决订票问题的最佳实践。
saga的理论来源
这种事务模式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模式,它们有不同的特点,各自适应不同的业务场景,相互补充。
上表对比了TCC和SAGA这两种交易模式。
TCC的定位是一致性要求高的短事务。一致性要求高的事务一般都是短事务(一个事务长期未完成,在用户眼里一致性比较差,一般不需要采用TCC的高一致性设计),所以TCC的事务分支排放在AP端(即程序代码),用户灵活调用。这样用户就可以根据每个分支的结果灵活判断和执行。
SAGA的定位是长事务/短事务,一致性要求低。对于预订机票这样的场景,持续时间长,可能持续几分钟到一两天。需要将整个事务安排保存到服务器中,避免因升级、故障等原因导致事务安排信息丢失。
状态机提供的灵活性对于客户端安排的TCC来说是不必要的,但是对于保存在服务器端的saga来说是有意义的。当我第一次设计saga时,我做了详细的权衡选择。这种状态机很难上手,用户很容易气馁。我找了一些用户做需求研究,总结出来的核心需求是:
子事务并发执行,减少延迟。比如旅游订票业务预订往返机票,因为订票可能需要很长时间才能确认,等机票订好了再订返程机票,很容易导致订不到。
有些操作不能回滚,需要放在可回滚子事务之后,保证一旦实施,最终会成功。
在这两个核心需求下,dtm的saga最终没有使用状态机,而是支持子事务的并发执行和指定子事务之间的顺序关系。
以实际问题为例,说明dtm中saga的用法。
对于订票业务,子事务的执行结果不是立即返还的,通常是第三方在订票后一段时间内通知结果。在这种情况下,dtm的saga提供了很好的支持,支持子事务返回的结果和指定的重试时间间隔。订票的子事务可以在自己的逻辑中。如果没有下单,就下单;如果已经下单,此时是重试请求,可以去第三方查询结果,最后返回成功/失败/进行。
解决问题实例
我们用一个真实的用户案例来讲解dtmsaga的最佳实践。
问题场景:一个用户旅行的应用,收到一个用户旅行计划,需要预订去三亚的机票,三亚的酒店,返程的机票。
要求:
两张机票和酒店要么预订成功,要么回滚(酒店和航空公司提供了相关的回滚界面)
预订机票和酒店并发,避免串行,因为某个预订的最终确认时间较晚,导致其他预订错过时间。
确认预定结果的时间可能从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。