深度剖析Saga分布式事务

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

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模式,它们有不同的特点,各自适应不同的业务场景,相互补充。
image.png

上表对比了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。

目录
相关文章
|
20天前
|
消息中间件 Dubbo 应用服务中间件
分布式事物【Hmily实现TCC分布式事务、Hmily实现TCC事务、最终一致性分布式事务解决方案】(七)-全面详解(学习总结---从入门到深化)
分布式事物【Hmily实现TCC分布式事务、Hmily实现TCC事务、最终一致性分布式事务解决方案】(七)-全面详解(学习总结---从入门到深化)
96 0
|
20天前
|
Java 数据库连接 API
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)
71 0
|
20天前
|
SQL 关系型数据库 MySQL
分布式事物【XA强一致性分布式事务实战、分布式架构的理论知识、TCC核心组成】(六)-全面详解(学习总结---从入门到深化)
分布式事物【XA强一致性分布式事务实战、分布式架构的理论知识、TCC核心组成】(六)-全面详解(学习总结---从入门到深化)
37 0
|
12月前
|
关系型数据库 Java 中间件
《微服务实战》 第三十章 分布式事务框架seata TCC模式
《微服务实战》 第三十章 分布式事务框架seata TCC模式
121 0
|
Java 中间件 数据库
谈谈分布式事务TCC机制
分布式事务是几乎所有分布式微服务系统中,最棘手也是最重要的一个点了。在讲解分布式事务前,先了解下数据库事务的特性;数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily),简称就是ACID。
167 0
谈谈分布式事务TCC机制
|
Java 数据库 开发者
分布式事务解决方案-TCC|学习笔记
快速学习分布式事务解决方案-TCC
114 0
分布式事务解决方案-TCC|学习笔记
|
JSON 数据格式
全网首发:Seata Saga状态机设计器实战(下)
全网首发:Seata Saga状态机设计器实战(下)
477 0
全网首发:Seata Saga状态机设计器实战(下)
|
JSON 监控 数据可视化
全网首发:Seata Saga状态机设计器实战(上)
全网首发:Seata Saga状态机设计器实战(上)
295 0
全网首发:Seata Saga状态机设计器实战(上)
|
SQL 数据采集 算法
搞懂Seata分布式事务AT、TCC、SAGA、XA模式选型
搞懂Seata分布式事务AT、TCC、SAGA、XA模式选型
417 0
搞懂Seata分布式事务AT、TCC、SAGA、XA模式选型
|
SQL 消息中间件 中间件
分布式事务中间件Seata的设计原理
因为待过的几家公司,用的都是微服务架构,但是在解决分布式事务的问题上都不太优雅,所以我也在一直关注 Seata 的发展,今天就简要说说它的一些设计上的原理,后续我将会对它的各个模块进行深入源码分析,感兴趣的可以持续关注我的公众号或者博客,不要跟丢。
256 0
分布式事务中间件Seata的设计原理