基于Saga的分布式事务实现方案

563 阅读13分钟

随着微服务架构的兴起,越来越多的公司会在实际场景中遇到分布式事务的问题。几个跨进程的应用共同完成一个任务,就更离不开分布式事务的参与。而对于分布式事务而言,2PC、TCC也是经常被提到了,不过在面对长业务流程,并且很难进行TCC改造的场景,会选择使用Saga分布式事务。

方案简介

1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理long lived transaction(长活事务)。Saga是一个长活事务可被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务。

论文地址:sagas

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga的组成

  • 每个Saga由一系列sub-transaction Ti 组成
  • 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果

可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

  • T1, T2, T3, ..., Tn
  • T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n

Saga定义了两种恢复策略

  • backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
  • forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。

理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机,网络可能会失败,甚至数据中心也可能会停电。在这种情况下我们能做些什么? 最后的手段是提供回退措施,比如人工干预。

Saga的使用条件

Saga看起来很有希望满足我们的需求。所有长活事务都可以这样做吗?这里有一些限制:

  1. Saga只允许两个层次的嵌套,顶级的Saga和简单子事务
  2. 在外层,全原子性不能得到满足。也就是说,sagas可能会看到其他sagas的部分结果
  3. 每个子事务应该是独立的原子行为
  4. 在我们的业务场景下,各个业务环境(如:航班预订、租车、酒店预订和付款)是自然独立的行为,而且每个事务都可以用对应服务的数据库保证原子操作。

补偿也有需考虑的事项:

  • 补偿事务从语义角度撤消了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态。(例如,如果事务触发导弹发射, 则可能无法撤消此操作)

但这对我们的业务来说不是问题。其实难以撤消的行为也有可能被补偿。例如,发送电邮的事务可以通过发送解释问题的另一封电邮来补偿。

处理流程

Saga 事务基本协议如下:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。

下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。

Saga 的执行顺序有两种,如上图:

  • 事务正常执行完成:T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。
  • 事务回滚:T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。

Saga 定义了两种恢复策略:

向前恢复(forward recovery)

对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn。如上图创建订单操作失败,进行多次重试后创建订单操作成功,接下来继续执行支付操作。该种情况下不需要Ci相关补偿操作。

向后恢复(backward recovery)

对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

Saga 事务的实现方法

实际上在启动一个Saga事务时,协调逻辑会告诉第一个Saga参与者,也就是子事务,去执行本地事务。事务完成之后Saga的会按照执行顺序调用Saga的下一个参与的子事务。这个过程会一直持续到Saga事务执行完毕。

如果在执行子事务的过程中遇到子事务对应的本地事务失败,则Saga会按照相反的顺序执行补偿事务。通常来说我们把这种Saga执行事务的顺序称为个Saga的协调逻辑。这种协调逻辑有两种模式,编排(Choreography)和控制(Orchestration)。

Saga 事务常见的实现方式有两种

  • 控制(Orchestrator)
  • 编排(Choreography)

控制 Orchestrator)

Saga提供一个控制类,其方便参与者之前的协调工作。事务执行的命令从控制类发起,按照逻辑顺序请求Saga的参与者,从参与者那里接受到反馈以后,控制类在发起向其他参与者的调用。所有Saga的参与者都围绕这个控制类进行沟通和协调工作。

中央协调器负责集中处理事件的决策和业务逻辑排序。中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。

以电商订单的例子为例:

  • 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
  • OSO 向库存服务请求扣减库存,库存服务回复处理结果。
  • OSO 向订单服务请求创建订单,订单服务回复创建结果。
  • OSO 向支付服务请求支付,支付服务回复处理结果。
  • 主业务逻辑接收并处理 OSO 事务处理结果回复。

中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。

  1. 在执行“支付”命令时发现“支付失败”,于是“支付服务”反馈给Saga协调器。

  2. 此时协调器调用“订单服务”中的“回滚”操作,完成订单失效的工作。

  3. 然后调用“库存服务”中的“回滚”完成库存回滚的工作

  4. 最后,通知主业务逻辑事务处理失败。

需要指出的是控制模式也是基于事件驱动的,与编排模式一样会发送消息通知参与者执行命令,上面两个图中命令的执行和调用也是通过消息的方式进行。

控制器设计的优点:

避免循环依赖:在编排模式中存在参与者之间的循环调用,而中心控制类的方式可以避免这种情况的发生。

降低复杂性:所有事务交给控制器完成,它负责命令的执行和回复的处理,参与者只需要完成自身的任务,不用考虑处理消息的方式,降低参与者接入的复杂性。

容易测试:测试工作集中在集中控制类上,其他服务单独测试功能即可。

容易扩展:如果事务需要添加新步骤,只需修改控制类,保持事务复杂性保持线性,回滚更容易管理。

控制器设计的缺点:

依赖控制器:控制器中集中太多逻辑的风险。

增加管理难度:这种模式除了管理各个业务服务以外,还需要额外管理控制类服务,无形中增加了管理的难度和复杂度。而且存在单点风险,一旦控制器出现问题,整个业务就处于瘫痪中。

事件编排(Event Choreography)

参与者(子事务)之间的调用、分配、决策和排序,通过事件消息进行。是一种去中心化的模式,参与者之间通过消息机制进行沟通,通过监听器的方式监听其他参与者发出的消息,从而执行后续的逻辑处理。由于没有中间协调点,靠参与靠自己进行相互协调。

没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

以电商订单的例子为例:

  • 事务发起方的主业务逻辑发布开始订单事件。
  • 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
  • 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
  • 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
  • 主业务逻辑监听订单已支付事件并处理。

  1. 假设在执行“支付”时子事务失败了,会发送“支付失败消息”。
  2. 订单服务在监听到“支付失败消息”之后会执行“回滚”的操作,完成订单失效的工作,发送“订单创建失败消息”。
  3. “库存服务”在监听到“订单创建失败消息”之后会执行“回滚”,同时发送“库存扣减失败消息”。
  4. 主业务监听“库存扣减失败消息”,走失败逻辑。
事件编排的优点:

简单:每个子事务进行操作时只用发布事件消息,其他子事务监听处理。

松耦合:参与者(服务)之间通过订阅事件进行沟通,组合会更加灵活。

事件编排的缺点:

理解困难:没有对业务流程进行完整的描述,要了解整个事务的执行过程需要通过阅读代码完成。增加开发人员理解和维护代码的难度。

存在服务的循环依赖:由于通过消息和事件进行沟通,参与者之间会存在循环依赖的情况。也就是A服务调用B服务,B服务又调用A服务的情况。这也增加了架构设计的复杂度,在设计初期需要认真考虑。

紧耦合风险:每个参与者执行的方法都依赖于上一步参与者发出的消息,但是上一步的参与者的所有消息都需要被订阅,才能了解参与者的真实状态,无形中增加了两个服务的耦合度。

方案总结

优势

  • 一阶段提交本地事务,无锁,高性能;
  • 参与者可异步执行,高吞吐;
  • 补偿服务易于实现,因为一个更新操作的反向操作是比较容易理解的。

缺点

  • 不保证事务的隔离性。

首先Saga是针对分布式长活事务的解决方案,针对事务长、多、复杂的情况,特别是服务由多个公司开发具有不可控性,可以使用Saga模式进行分布式事务的处理。Saga在处理事务一致性方面采取了向前恢复和向后恢复策略,前者通过不断重试的方式保证事务完成,而后者通过子事务的补偿事务,逐一回滚的方式让事务标记失败。在分布式协调方面,Saga采用了两种模式:编排和控制。前者让参与者(服务)之间通过消息进行沟通,根据事件出发事务的执行流程,是一种去中心化的模式。后者通过中心控制类,处理事务的执行和回滚步骤,统一调用服务和接受服务的反馈。

Saga对ACID的保证

Saga对于ACID的保证和TCC一样:

  • 原子性(Atomicity):正常情况下保证。
  • 一致性(Consistency),在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的。
  • 隔离性(Isolation),在某个时间点,A事务能够读到B事务部分提交的结果。
  • 持久性(Durability),和本地事务一样,只要commit则数据被持久。

Saga不提供ACID保证,因为原子性和隔离性不能得到满足。原论文描述如下:

full atomicity is not provided. That is, sagas may view the partial results of other sagas

通过saga log,saga可以保证一致性和持久性。

Saga和TCC对比

Saga相比TCC的缺点是缺少预留动作,导致补偿动作的实现比较麻烦:Ti就是commit,比如一个业务是发送邮件,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci),实现起来有一些麻烦。

如果把上面的发邮件的例子换成:A服务在完成Ti后立即发送Event到ESB(企业服务总线,可以认为是一个消息中间件),下游服务监听到这个Event做自己的一些工作然后再发送Event到ESB,如果A服务执行补偿动作Ci,那么整个补偿动作的层级就很深。

不过没有预留动作也可以认为是优点:

  • 有些业务很简单,套用TCC需要修改原来的业务逻辑,而Saga只需要添加一个补偿动作就行了。
  • TCC最少通信次数为2n,而Saga为n(n=sub-transaction的数量)。
  • 有些第三方服务没有Try接口,TCC模式实现起来就比较tricky了,而Saga则很简单。
  • 没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单(请对比Saga的恢复策略和TCC的异常处理)。