Saga分布式事务

一、简介

与分布式事务TCC一样,目的都是为了在各个服务中正常使用事务。和TCC相比,Saga没有“预留”动作,操作都是直接提交到库。其中:

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

既然Saga的操作都是直接提交到库中,那么当后续的服务操作失败时,我们需要一种方法将已被改变的值更改为之前的状态。

为此Saga定义了两种恢复策略:

  • backward recovery:向后恢复,补偿所有已完成的事务,如果任一子事务失败,则撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
  • forward recovery:向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,此处不需要补偿事务。

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

注意事项:

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

  1. Ti和Ci是幂等的。假设在执行Ti的时候超时了,如果采用重传策略则会再次发送Ti,那么就有可能出现Ti被执行了两次,所以要求Ti幂等。而如果Ci也超时了,就会尝试再次发送Ci,那么就有可能出现Ci被执行两次,所以要求Ci幂等。
  2. Ci必须是能够成功的,如果无法成功则需要人工介入。因为如果Ci不能执行成功就意味着整个Saga无法完全撤销,这个是不允许的
  3. Ti - Ci和Ci - Ti的执行结果必须是一样的:sub-transaction被撤销了。若Ti执行超时后使用补偿策略则会有三种情况:Ti的请求丢失了,服务之前没有、之后也不会执行Ti、Ti在Ci之前执行、Ci在Ti之前执行。针对后两种情况,要求Ti和Ci是可交换的,并且其最终结果都是sub-transaction被撤销。

在Saga的使用中存在一些限制:

  1. Saga只允许两个层次的嵌套,顶级的Saga和简单子事务
  2. 在不适用隔离策略的情况下不能够保证ACID中的隔离性。
  3. 每个子事务应该是独立的原子行为。
  • 补偿事务从语义角度撤消了事务Ti的行为,但未必能将数据库返回到执行Ti时的状态。但应该尽量补偿。

对于ACID的保证:

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

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

和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的数量)。
  • 没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单(TCC需要释放预留资源)。

二、实现

2.1 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 log中,我们可以将saga恢复到上述任何状态。

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

2.2 可靠的通讯

在实施基于编排的saga时,您必须考虑一些与服务间通信相关的问题。

  • 第一个问题是确保saga参与者更新其数据库并将事件作为数据库事务的一部分发布。
  • 第二个问题是确保saga参与者必须能够将收到的每个事件映射到自己的数据。

三、Saga协调

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

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

后续我们讲解该两种协调方式,以及以一个带有支付服务(PayService)、订单服务(OrderService)、库存服务(InventoryService)的电商系统为例进行说明。

3.1 编排

使用编排时,没有中央协调员告诉saga参与者该做什么。相反,sagas参与者订阅彼此的事件并做出相应的响应。以一个带有支付服务(PayService)、订单服务(OrderService)、库存服务(InventoryService)的电商系统为例:

正常流程:

  • OrderService在APPROVAL_PENDING状态下创建一个Order并发布OrderCreated事件。
  • PayService消费OrderCreated事件,验证顾客是否进行支付,并发布PayVerified事件。
  • InventoryService消费OrderCreated事件,减少库存,并发布InventoryVerified事件。
  • OrderService消费PayVerified和InventoryVerified事件,更改订单状态到APPROVED。

出错流程:

  • OrderService在APPROVAL_PENDING状态下创建一个Order并发布OrderCreated事件。
  • PayService消费OrderCreated事件,验证顾客是否进行支付,并发布PayVerified事件。
  • InventoryService消费OrderCreated事件,减少库存,并发布InventoryReject事件。
  • OrderService消费PayVerified和InventoryReject事件,并发布事件到REJECT。
  • PayService消费InventoryReject事件,补偿支付操作。
  • OrderService消费InventoryReject事件,将订单状态更改为REJECT。

编排的saga的好处和缺点

基于编排的saga有几个好处

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

并且有一些缺点

  • 更难理解:与业务流程不同,代码中没有一个地方可以定义saga,因为编排在服务中通过通过订阅发布分配saga的实,开发人员有时很难理解给定的saga是如何工作的。
  • 服务之间的循环依赖关系:saga参与者订阅彼此的事件,这通常会创建循环依赖关系。尽管问题不大,但还是会被认为的设计的问题。
  • 紧密耦合的风险:每个saga参与者都需要订阅所有影响他们的事件。例如,订单必须订阅导致交易流程中失败的所有事件。因此,存在一种风险,即需要与Order Service实施的订单生命周期保持同步更新。

3.2 控制

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

正常流程:

  1. Order Service首先创建一个Order和一个创建订单控制器。
  2. saga orchestrator向PayService发送Verify Pay命令。
  3. PayService回复Verify Pay消息。
  4. saga orchestrator向InventoryService发送Verify Inventory命令。
  5. InventoryService回复Verify Inventory消息。
  6. saga orchestrator向订单服务发送批准订单命令。

使用状态机建模SAGA ORCHESTRATORS

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

以上例子状态机可由以下部分组成:

  • Verifying Pay:初始状态。当处于此状态时,该saga正在等待支付结果。
  • Verifying Inventory:该saga正在等待库存减少的结果。
  • OrderApproved:表示saga成功完成的最终状态。
  • Order Rejected:最终状态表明该订单被其中一方参与者们拒绝。

使用saga编排的好处和缺点。

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

  1. 更简单的依赖关系:编排的一个好处是它不会引入循环依赖关系。 saga orchestrator调用saga参与者,但参与者不会调用orchestrator。因此,协调器依赖于参与者,但反之亦然,因此没有循环依赖性。
  2. 较少的耦合:每个服务都实现了由orchestrator调用的API,因此它不需要知道saga参与者发布的事件。
  3. 改善关注点分离并简化业务逻辑:saga协调逻辑本地化在saga协调器中。域对象更简单,并且不了解它们参与的saga。

业务流程也有一个缺点

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

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

四、参考资料

分布式事务:Saga模式

拜托,面试请不要再问我TCC分布式事务的实现原理!

你可能感兴趣的:(分布式)