10分钟说透Saga分布式事务
点击上方蓝色“ 石杉的架构笔记 ”,选择“设为星标”
回复“PDF”获取独家整理的学习资料!
长按扫描上方二维码一元抢购
开篇
随着微服务架构的兴起,越来越多的公司会在实际场景中遇到分布式事务的问题。特别是在金融应用场景,几个跨进程的应用共同完成一个任务,就更离不开分布式事务的参与。而对于分布式事务而言,2PC、TCC也是经常被提到了,不过在面对长业务流程,并且很难进行TCC改造的场景,会选择使用Saga分布式事务。今天会给大家介绍Saga实现分布式事务的内容:
-
Saga的分布式解决方案 -
Saga处理事务一致性 -
Saga分布式事务协调
Saga的分布式解决方案
随着互联网的快速发展,原来的单体应用已经很难支撑大流量高并发的请求了,因此软件系统由原来的单体应用逐渐向分布式过度,如图1所示,左边的Web App 包含了UI和服务的模块,在转变以后会对应右边的微服务架构,服务之间存在关联地相互调用。
图1 单体到分布式的系统架构过渡
在进行分布式部署之后,会存在多个服务共同完成一个事务操作,并且这些服务彼此都存在于不同的服务器或者网络环境,服务之间需要通过网络远程协作完成事务称之为分布式事务。例如:银行转账业务、下单扣件库存等。
在分布式事务场景下,如果对数据有强一致性要求,会在业务层上才去“两阶段提交”(2PC)的方案。
如果保证最终一致性的话可以采取TCC (Try Confirm Cancel)模式。虽然TCC保证最终一致性的模式被业内广泛使用,但是对于某些分布式事务场景,流程多、流程长、还可能要调用其它公司的服务。特别是对于不可控的服务(其他公司的服务),这些服务无法遵循 TCC 开发模式,导致TCC模式的开发成本增高。体现在具体场景中,以金融核心的业务为代表(渠道层、产品层、集成层),其特点是:流程多、流程长、调用不可控服务。同时也是应为流程长,事务边界太长,加锁时间长,使用TCC模式会影响并发性能。
鉴于此类业务场景的分布式事务处理,提出了Saga分布式处理模式。Saga是一种“长事务的解决方案”,更适合于“业务流程长、业务流程多”的场景。特别是针对参与事务的服务是遗留系统服务,此类服务无法提供TCC模式下的三个接口,就可以采用Saga模式。
其适用于的业务业务场景有,金融机构对接系统(需要对接外部系统)、渠道整合(流程长)、分布式架构服务等。其优势是一阶段提交本地事务,无锁,高性能;参与者可异步执行,高吞吐;补偿服务易于实现,因为一个更新操作的反向操作是比较容易理解的;当然其也存在缺点,就是不保证隔离性。
Saga处理事务一致性
1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理long lived transaction(长活事务)。Saga是一个长活事务可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。
在这位老兄的论文中提到,每个Saga由一系列sub-transaction Ti组成。每个Ti都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果。这里可以理解为,针对每一个分布式事务的每个执行操作或者是步骤都是一个 Ti,例如扣减库存是T1、创建订单是T2、支付服务是T3。那么针对每个Ti都对应一个补偿动作Ci,例如回复库存C1、订单回滚C2、支付回滚C3。
Saga事务有两种恢复策略:
向前恢复(forward recovery),也就是“勇往直前”。
对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。这种方式适用于必须要成功的场景,如图2 所示,上面的图例,子事务按照从左到右的顺序执行,T1执行完毕以后T2 执行,然后是T3、T4、T5。
图2 Saga事务执行的策略
事务恢复的顺序也是按照:T1、T2、T3、T4、T5的方向进行,如果在执行T1的时候失败了就重试T1,以此类推在哪个子事务执行时失败了就执行哪个事务。因此叫做“勇往直前”。
向后恢复(backward recovery),在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。如图2所示,下面的图例,子事务依旧从左往右执行,在执行到事务T3的时候,该事务执行失败了,于是按照红线的方向开始执行补偿事务,先执行C3、然后是C2和C1,直到T0、T1、T2的补偿事务C1、C2、C3都执行完毕。也就是回滚整个Saga的执行结果。
Saga分布式事务协调
上面介绍了Saga的概念和事务恢复方式,每个事务存在多个子事务,每个子事务都有一个补偿事务,其在事务回滚的时候使用。由于子事务对应的操作在分布式的系统架构中会部署在不同的服务中,这些子事务为了完成共同的事务需要进行协同。
实际上在启动一个Saga事务时,协调逻辑会告诉第一个Saga参与者,也就是子事务,去执行本地事务。事务完成之后Saga的会按照执行顺序调用Saga的下一个参与的子事务。这个过程会一直持续到Saga事务执行完毕。
如果在执行子事务的过程中遇到子事务对应的本地事务失败,则Saga会按照相反的顺序执行补偿事务。通常来说我们把这种Saga执行事务的顺序称为个Saga的协调逻辑。这种协调逻辑有两种模式,编排(Choreography)和控制(Orchestration)分别如下:
编排(Choreography):参与者(子事务)之间的调用、分配、决策和排序,通过交换事件进行进行。是一种去中心化的模式,参与者之间通过消息机制进行沟通,通过监听器的方式监听其他参与者发出的消息,从而执行后续的逻辑处理。由于没有中间协调点,靠参与靠自己进行相互协调。
控制(Orchestration):Saga提供一个控制类,其方便参与者之前的协调工作。事务执行的命令从控制类发起,按照逻辑顺序请求Saga的参与者,从参与者那里接受到反馈以后,控制类在发起向其他参与者的调用。所有Saga的参与者都围绕这个控制类进行沟通和协调工作。
下面通过一个例子来介绍这两种协调模式,假设有一个下单的业务,从订单服务的创建订单操作发起,会依次调用支付服务中的支付订单,库存服务中的扣减库存以及发货服务中的发货操作,最终如果所有参与者(服务)中的操作(子事务)完成的话,整个下单事务就算完成。
编排(Choreography),由于没有中心的控制类参与参与者操作之间的协调工作,因此通过消息发送的方式进行协调。
如图3所示:
图3 编排模式-事务执行成功
1. “订单服务”中执行“创建订单”操作,此时会发送一个“创建订单消息”到队列中。
2. “支付服务”监听到队列中的这个订单消息,调用“支付订单”的操作,同时也发送“只服务消息”到队列中。
3. “库存服务”在监听到“支付消息”之后会进行“扣减库存”的处理,并且发送“扣减库存消息”等待下一个消费者接受。
4. “发货服务”作为整个事务的最后一个子事务,在接到“扣减库存消息”以后会执行发货的子事务,完成事务以后会给“订单服务”发送“发货消息”,订单服务在接受到消息以后完成整个事务闭环,并且提交。
上面说的是事务执行成功的情况,如果事务执行失败那应该如何处理?
如图4所示:
图4 编排模式-事务执行失败
1. 假设在执行“发货”时子事务失败了,会发送“发货失败消息”。
2. 库存服务在接受到“发货失败消息”之后会执行“回滚库存”的操作,该操作将原来扣减的库存加回去,同时发送“扣减失败消息”。
3. “支付服务”在接受到“扣减失败消息”之后会执行“回滚支付”,进行退款的操作,同时发送“支付失败消息”。订单服务在接受到该消息以后将下单事务标记为失败。
从上面的描述可以看出编排的好处:
简单:每个子事务进行操作时只用发布事件消息,其他子事务监听处理。
松耦合:参与者(服务)之间通过订阅事件进行沟通,组合会更加灵活。
当然也有一些缺点:
理解困难:没有对业务流程进行完整的描述,要了解整个事务的执行过程需要通过阅读代码完成。增加开发人员理解和维护代码的难度。
存在服务的循环依赖:由于通过消息和事件进行沟通,参与者之间会存在循环依赖的情况。也就是A服务调用B服务,B服务又调用A服务的情况。这也增加了架构设计的复杂度,在设计初期需要认真考虑。
紧耦合风险:每个参与者执行的方法都依赖于上一步参与者发出的消息,但是上一步的参与者的所有消息都需要被订阅,才能了解参与者的真实状态,无形中增加了两个服务的耦合度。
控制(Orchestration),其核心是定义一个控制类,它会告诉参与者(服务)应该执行哪些操作(子事务)。 Saga控制类通过命令以及异步回复的方式与参与者进行交互。
如图5所示:
图5 控制模式-成功
1. 订单服务执行下单事务时,向Saga协调器发送请求命令,Saga协调器接受到命令以后按照子事务执行的顺序调用服务中的方法。
2. 最开始执行“支付订单”的操作,调用“支付服务”中的“支付订单”操作,并且通过虚线的部分返回执行结果“支付完成”。
3. 接下来,执行“库存服务”中的“扣减库存”方法,同样通过虚线部分返回扣减完成的消息给“请求反馈“模块。
4. 紧接着就是执行“发货“命令,调用”发货服务“中的”发货“方法,并且返回”发货完成“的响应。
5. 最后,三个子事务都执行完毕以后,返回订单服务,完成整个分布式事务。
介绍完成成功完成事务之后,再来看看出现异常的情况。
如图6所示:
图6 控制模式-失败
1. 在执行“发货”命令时发现“发货失败”,于是“发货服务”反馈给Saga协调器。
2. 此时协调器调用“库存服务”中的“回滚库存”操作,将扣减的库存恢复。
3. 然后调用“支付服务”中的“回滚支付”完成支付退款的工作。
4. 最后,通知订单服务事务处理失败。
需要指出的是控制模式也是基于事件驱动的,与编排模式一样会发送消息通知参与者执行命令,上面两个图中命令的执行和调用也是通过消息的方式进行。
控制器设计的优点:
避免循环依赖:在编排模式中存在参与者之间的循环调用,而中心控制类的方式可以避免这种情况的发生。
降低复杂性:所有事务交给控制器完成,它负责命令的执行和回复的处理,参与者只需要完成自身的任务,不用考虑处理消息的方式,降低参与者接入的复杂性。
容易测试:测试工作集中在集中控制类上,其他服务单独测试功能即可。
容易扩展:如果事务需要添加新步骤,只需修改控制类,保持事务复杂性保持线性,回滚更容易管理。
当然这种方法也存在缺点:
依赖控制器:控制器中集中太多逻辑的风险。
增加管理难度:这种模式除了管理各个业务服务以外,还需要额外管理控制类服务,无形中增加了管理的难度和复杂度。而且存在单点风险,一旦控制器出现问题,整个业务就处于瘫痪中。
总结
END
如有收获,请划至底部,点击“在看”,谢谢!