深度剖析 Saga 分布式事务 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
dongfuye1
V2EX    推广

深度剖析 Saga 分布式事务

  •  
  •   dongfuye1 2021-11-25 09:50:11 +08:00 1791 次点击
    这是一个创建于 1421 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 不友好( gRPC 协议,在 TM 拿不到用户自定义的输入输出 pb 文件,因此无法解析结果中的字段)

    非状态机实现

    这一类的实现有 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 。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2745 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 14:34 PVG 22:34 LAX 07:34 JFK 10:34
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86