1 Saga相关概念

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

1.1 Saga的组成

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

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

Saga的执行顺序有两种:

  • T, T, T, ..., T
  • T, T, ..., T, C,..., C, C,其中0 < j < n

Saga定义了两种恢复策略:

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

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

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

1.2 Saga的使用条件

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

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

补偿也有需考虑的事项:

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

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

对于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可以保证一致性和持久性。

和TCC对比

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

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

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

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

2 Saga相关实现

Saga Log

Saga保证所有的子事务都得以完成或补偿,但Saga系统本身也可能会崩溃。Saga崩溃时可能处于以下几个状态:

  • Saga收到事务请求,但尚未开始。因子事务对应的微服务状态未被Saga修改,我们什么也不需要做。
  • 一些子事务已经完成。重启后,Saga必须接着上次完成的事务恢复。
  • 子事务已开始,但尚未完成。由于远程服务可能已完成事务,也可能事务失败,甚至服务请求超时,saga只能重新发起之前未确认完成的子事务。这意味着子事务必须幂等。
  • 子事务失败,其补偿事务尚未开始。Saga必须在重启后执行对应补偿事务。
  • 补偿事务已开始但尚未完成。解决方案与上一个相同。这意味着补偿事务也必须是幂等的。
  • 所有子事务或补偿事务均已完成,与第一种情况相同。

为了恢复到上述状态,我们必须追踪子事务及补偿事务的每一步。我们决定通过事件的方式达到以上要求,并将以下事件保存在名为saga log的持久存储中:

  • Saga started event 保存整个saga请求,其中包括多个事务/补偿请求
  • Transaction started event 保存对应事务请求
  • Transaction ended event 保存对应事务请求及其回复
  • Transaction aborted event 保存对应事务请求和失败的原因
  • Transaction compensated event 保存对应补偿请求及其回复
  • Saga ended event 标志着saga事务请求的结束,不需要保存任何内容

分布式事务:Saga模式-LMLPHP

通过将这些事件持久化在saga log中,我们可以将saga恢复到上述任何状态。

由于Saga只需要做事件的持久化,而事件内容以JSON的形式存储,Saga log的实现非常灵活,数据库(SQL或NoSQL),持久消息队列,甚至普通文件可以用作事件存储, 当然有些能更快得帮saga恢复状态。

注意事项

对于服务来说,实现Saga有以下这些要求:

  1. T和C是幂等的。
  2. C必须是能够成功的,如果无法成功则需要人工介入。
  3. T - C和C - T的执行结果必须是一样的:sub-transaction被撤销了。

第一点要求T和C是幂等的,举个例子,假设在执行T的时候超时了,此时我们是不知道执行结果的,如果采用forward recovery策略就会再次发送T,那么就有可能出现T被执行了两次,所以要求T幂等。如果采用backward recovery策略就会发送C,而如果C也超时了,就会尝试再次发送C,那么就有可能出现C被执行两次,所以要求C幂等。

第二点要求C必须能够成功,这个很好理解,因为,如果C不能执行成功就意味着整个Saga无法完全撤销,这个是不允许的。但总会出现一些特殊情况比如C的代码有bug、服务长时间崩溃等,这个时候就需要人工介入了。

第三点乍看起来比较奇怪,举例说明,还是考虑T执行超时的场景,我们采用了backward recovery,发送一个C,那么就会有三种情况:

  1. T的请求丢失了,服务之前没有、之后也不会执行T
  2. T在C之前执行
  3. C在T之前执行

对于第1种情况,容易处理。对于第2、3种情况,则要求T和C是可交换的(commutative),并且其最终结果都是sub-transaction被撤销。

3 Saga协调

协调saga:saga的实现包含协调saga步骤的逻辑。当系统命令启动saga时,协调逻辑必须选择并告知第一个saga参与者执行本地事务。一旦该事务完成,saga的排序协调选择并调用下一个saga参与者。这个过程一直持续到saga执行了所有步骤。如果任何本地事务失败,则saga必须以相反的顺序执行补偿事务。构建一个saga的协调逻辑有几种不同的方法:

  • 编排(Choreography):在saga参与者中分配决策和排序。他们主要通过交换事件进行沟通。
  • 控制(Orchestration):在saga控制类中集中saga的协调逻辑。一个saga控制者向saga参与者发送命令消息,告诉他们要执行哪些操作。

3.1 编排(Choreography)

基于编排的saga:实现sagas的一种方法是使用编排。当使用编排时,没有中央协调员告诉saga参与者该做什么。相反,sagas参与者订阅彼此的事件并做出相应的响应。

分布式事务:Saga模式-LMLPHP

通过这个sagas的路径如下:

  1. Order Service在APPROVAL_PENDING状态下创建一个Order并发布OrderCreated事件。
  2. Consumer Service消费OrderCreated事件,验证消费者是否可以下订单,并发布ConsumerVerified事件。
  3. Kitchen Service消费OrderCreated事件,验证订单,在CREATE_PENDING状态下创建故障单,并发布TicketCreated事件。
  4. Accounting服务消费OrderCreated事件并创建一个处于PENDING状态的Credit CardAuthorization。
  5. Accounting Service消费TicketCreated和ConsumerVerified事件,收取消费者的信用卡,并发布信用卡授权活动。
  6. Kitchen Service使用CreditCardAuthorized事件并更改AWAITING_ACCEPTANCE票的状态。
  7. Order Service收到CreditCardAuthorized事件,更改订单状态到APPROVED,并发布OrderApproved事件。

创建订单saga还必须处理saga参与者拒绝订单并发布某种失败事件的场景。例如,消费者信用卡的授权可能会失败。saga必须执行补偿交易以撤消已经完成的事情。图中显示了AccountingService无法授权消费者信用卡时的事件流。

 分布式事务:Saga模式-LMLPHP

事件顺序如下:

  1. Order服务在APPROVAL_PENDING状态下创建一个Order并发布OrderCreated事件。
  2. Consumer服务消费OrderCreated事件,验证消费者是否可以下订单,并发布ConsumerVerified事件。
  3. Kitchen服务消费OrderCreated事件,验证订单,在CREATE_PENDING状态下创建故障单,并发布TicketCreated事件。
  4. Accounting服务消费OrderCreated事件并创建一个处于PENDING状态的Credit CardAuthorization。
  5. Accounting服务消费TicketCreated和ConsumerVerified事件,向消费者的信用卡收费,并发布信用卡授权失败事件。
  6. Kitchen服务使用信用卡授权失败事件并将故障单的状态更改为REJECTED。
  7. 订单服务消费信用卡授权失败事件,并将订单状态更改为已拒绝。

可靠的基于事件的通信

在实施基于编排的saga时,您必须考虑一些与服务间通信相关的问题。第一个问题是确保saga参与者更新其数据库并将事件作为数据库事务的一部分发布。
您需要考虑的第二个问题是确保saga参与者必须能够将收到的每个事件映射到自己的数据。

编组的saga的好处和缺点

基于编舞的saga有几个好处

  • 简单:服务在创建,更新或删除业务时发布事件对象
  • 松耦合:参与者订阅事件并且彼此之间没有直接的了解。

并且有一些缺点

  • 更难理解:与业务流程不同,代码中没有一个地方可以定义saga。相反,编排在服务中分配saga的实现。因此,开发人员有时很难理解给定的saga是如何工作的。
  • 服务之间的循环依赖关系:saga参与者订阅彼此的事件,这通常会创建循环依赖关系。例如,如果仔细检查图示,您将看到存在循环依赖关系,例如订单服务、会计服务、订单服务。虽然这不一定是个问题,但循环依赖性被认为是设计问题。
  • 紧密耦合的风险:每个saga参与者都需要订阅所有影响他们的事件。例如,会计服务必须订阅导致消费者信用卡被收费或退款的所有事件。因此,存在一种风险,即需要与Order Service实施的订单生命周期保持同步更新。

3.2 控制(Orchestration)

控制是实现sagas的另一种方式。使用业务流程时,您可以定义一个控制类,其唯一的职责是告诉saga参与者该做什么。 saga控制使用命令/异步回复样式交互与参与者进行通信。

 分布式事务:Saga模式-LMLPHP

  • Order Service首先创建一个Order和一个创建订单控制器。之后,路径的流程如下:
  • saga orchestrator向Consumer Service发送Verify Consumer命令。
  • Consumer Service回复Consumer Verified消息。
  • saga orchestrator向Kitchen Service发送Create Ticket命令。
  • Kitchen Service回复Ticket Created消息。
  • saga协调器向Accounting Service发送授权卡消息。
  • Accounting服务部门使用卡片授权消息回复。
  • saga orchestrator向Kitchen Service发送Approve Ticket命令。
  • saga orchestrator向订单服务发送批准订单命令。

使用状态机建模SAGA ORCHESTRATORS

建模saga orchestrator的好方法是作为状态机。状态机由一组状态和一组由事件触发的状态之间的转换组成。每个transition都可以有一个action,对于一个saga来说是一个saga参与者的调用。状态之间的转换由saga参与者执行的本地事务的完成触发。当前状态和本地事务的特定结果决定了状态转换以及执行的操作(如果有的话)。对状态机也有有效的测试策略。因此,使用状态机模型可以更轻松地设计、实施和测试。

分布式事务:Saga模式-LMLPHP

图显示了Create Order Saga的状态机模型。此状态机由多个状态组成,包括以下内容:

  • Verifying Consumer:初始状态。当处于此状态时,该saga正在等待消费者服务部门验证消费者是否可以下订单。
  • Creating Ticket:该saga正在等待对创建票证命令的回复。
  • Authorizing Card:等待Accounting服务授权消费者的信用卡。
  • OrderApproved:表示saga成功完成的最终状态。
  • Order Rejected:最终状态表明该订单被其中一方参与者们拒绝。

SAGA ORCHESTRATION和TRANSACTIONAL MESSAGING

基于业务流程的saga的每个步骤都包括更新数据库和发布消息的服务。例如,Order Service持久保存Order和Create Order Saga orchestrator,并向第一个saga参与者发送消息。一个saga参与者,例如Kitchen Service,通过更新其数据库并发送回复消息来处理命令消息。 Order Service通过更新saga协调器的状态并向下一个saga参与者发送命令消息来处理参与者的回复消息。服务必须使用事务性消息传递,以便自动更新数据库并发布消息。

让我们来看看使用saga编排的好处和缺点。

基于ORCHESTRATION的SAGAS的好处和缺点

基于编排的saga有几个好处:

  1. 更简单的依赖关系:编排的一个好处是它不会引入循环依赖关系。 saga orchestrator调用saga参与者,但参与者不会调用orchestrator。因此,协调器依赖于参与者,但反之亦然,因此没有循环依赖性。
  2. 较少的耦合:每个服务都实现了由orchestrator调用的API,因此它不需要知道saga参与者发布的事件。
  3. 改善关注点分离并简化业务逻辑:saga协调逻辑本地化在saga协调器中。域对象更简单,并且不了解它们参与的saga。例如,当使用编排时,Order类不知道任何saga,因此它具有更简单的状态机模型。在执行创建订单saga期间,它直接从APPROVAL_PENDING状态转换到APPROVED状态。 Order类没有与saga的步骤相对应的任何中间状态。因此,业务更加简单。

业务流程也有一个缺点

  • 在协调器中集中过多业务逻辑的风险。这导致了一种设计,其中智能协调器告诉哑巴服务要做什么操作。幸运的是,您可以通过设计独立负责排序的协调器来避免此问题,并且不包含任何其他业务逻辑。

除了最简单的saga,我建议使用编排。为您的saga实施协调逻辑只是您需要解决的设计问题之一。

05-11 17:45