数据库专家分布式事务php

分布式事务:Saga模式

2018-11-28  本文已影响635人  shoukai

1 Saga相关概念

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

1.1 Saga的组成

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

Saga的执行顺序有两种:

Saga定义了两种恢复策略:

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

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

1.2 Saga的使用条件

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

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

补偿也有需考虑的事项:

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

对于ACID的保证:

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

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

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

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

和TCC对比

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

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

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

2 Saga相关实现

Saga Log

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

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

66ae7b320e502c13f4a21a08baa61ead

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

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

注意事项

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

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

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

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

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

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

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

3 Saga协调

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

3.1 编排(Choreography)

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

Screen Shot 2018-11-27 at 23.24.17

通过这个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无法授权消费者信用卡时的事件流。

Screen Shot 2018-11-27 at 23.54.12

事件顺序如下:

  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有几个好处

并且有一些缺点

3.2 控制(Orchestration)

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

Screen Shot 2018-11-28 at 00.08.51
  1. Order Service首先创建一个Order和一个创建订单控制器。之后,路径的流程如下:
  2. saga orchestrator向Consumer Service发送Verify Consumer命令。
  3. Consumer Service回复Consumer Verified消息。
  4. saga orchestrator向Kitchen Service发送Create Ticket命令。
  5. Kitchen Service回复Ticket Created消息。
  6. saga协调器向Accounting Service发送授权卡消息。
  7. Accounting服务部门使用卡片授权消息回复。
  8. saga orchestrator向Kitchen Service发送Approve Ticket命令。
  9. saga orchestrator向订单服务发送批准订单命令。

使用状态机建模SAGA ORCHESTRATORS

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

Screen Shot 2018-11-28 at 00.12.58

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

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实施协调逻辑只是您需要解决的设计问题之一。

4 参考示例

上一篇下一篇

猜你喜欢

热点阅读