zhaoyu@home:~$

微服务设计模式-sagas处理分布式事务

saga:使用一系列通过异步消息通信的本地事务,维持数据的一致性。系统操作触发sage的第一步,一个本地事务完成后,接着执行下 一个本地事务。

sage在多个服务之间处理事务时,缺少了传统事务的隔离性(读未提交,读已提交,可重复度,序列化),会导致存在并发问题,和传统 的并发问题一样会导致数据不可预测。

下图是使用saga来创建订单,创建订单时,会牵扯到其他服务的事务。如下图:

该步骤由下面的事务组成:

  1. 订单服务——创建一个<创建待定>状态的订单。
  2. 消费者服务——验证消费者是否可以发起一个订单。
  3. 厨房服务——创建一个<创建待定>的小票。
  4. 账户服务——授权消费者的信用卡。
  5. 厨房服务——将小票的状态修改为<等待接收>。
  6. 订单服务——将订单状态修改为<创建成功>

saga中的每个事务都是独立提交的,所以saga必须可以通过补偿事务回滚修改,每个本地事务需要提供对应的回滚事务,失败后会按照事务 执行的顺序,反序执行执行回滚。并不一定每个事务都需要回滚。

如果创建订单的动作在验证账号的时候失败,那么saga就会执行相应的回滚操作。

  1. 订单服务——创建一个<创建待定>状态的订单。
  2. 消费者服务——验证消费者是否可以发起一个订单。
  3. 厨房服务——创建一个<创建待定>的小票。
  4. 账户服务——授权消费者的信用卡,失败。
  5. 厨房服务——将小票的状态修改为<创建-拒绝>。
  6. 订单服务——将订单状态修改为<拒绝>

saga的协同

saga必须协同本地事务执行的步骤,在一个事务执行完后,调用另一个事务,当后事务执行失败后,执行补偿事务回滚,saga协同动作,有 多种实现方式,有choreography(编排)和orchestration(编制)。

  • choreography(编排),参与方在无中心控制点的情况下,交换事件(event)。
  • orchestration(编制),有一个集中控制器告诉参与者要执行的操作。

基于编排的方式

编排没有中心的协调者,saga的参与者通过订阅彼此的事件,做出对应的响应。 如创建订单的saga如下:各个参与方通过交换事件来通信。

上图中,执行步骤如下:

  • Order服务发布订单创建事件。Consumer,Kitchen,Accounting服务监听订单创建消息。执行2,3,4,步骤。
  • Consumer服务监听订单创建事件,执行验证,发布Consumer事件。
  • kitchen服务监听订单创建事件,验证Order,创建一个<创建待定>的小票。发布小票创建事件。
  • Account服务监听订单创建事件,授权信用卡等。
  • Account服务监听小票创建事件和Consumer验证事件。执行信用卡授权成功。
  • Kitchen服务监听支付事件。将小票状态改为<等待接收>。
  • Order服务监听支付事件,修改订单状态为<创建成功>。至此,所有步骤完成。

假如有步骤执行失败,那么对应的监听服务会监听到执行失败的事件。执行对应失败的动作。

基于编排saga的优缺点

优点:

  • 简单——服务只要在他们创建和修改数据的时候发布事件。
  • 解耦——参与方只需要订阅感兴趣的事件,不必关心其他的参与方。

缺点:

  • 和编制相比,很难理解,没有一个集中的地方来定义saga。
  • 服务之间可能存在循环依赖。

总体来看,编排适合简单的saga场景。

基于编制的方式

当使用编制时,我们可以定义个单一责任的编制器。告诉saga的参与者如何做。编制器通过异步响应的方式和参与者交互。要执行一个 saga步骤,编制器需要发送一个命令消息给参与者,告诉参与者需要执行什么操作。当参与者执行完后,它会回复消息给编制器。编制器 根据回复再考虑下一步如何做。

还是以创建订单为例,执行步骤如下图。

基于编制的saga使用CreateOrderSaga类通过异步request/response的方式对整个流程进行编制。saga编制类发送命令给各参与者,如 Kitchen服务和Consumer服务。然后从channel中读取回复消息并决定下一步的动作。如果一切顺利,成功的执行步骤如下:

订单服务先创建一个订单 和 Order Saga 编排器。接下来:

  • saga 编排器发送一个Verify Consumer 命令给Consumer服务。
  • Consumer服务返回Consumer Verified消息。
  • Saga编排器发送一个创建小票的命令给Kitchen服务。
  • Kitchen服务返回一个小票创建的消息。
  • Saga编排器发送一个信用卡授权的消息给Accounting 服务。
  • Accounting服务返回一个信用卡授权成功的消息。
  • Saga编排器向Kitchen服务发送小票创建成功的消息。
  • Saga编排器向订单服务发送订单创建服务的消息。

基于编制的saga每一步都是一个更新数据库和发送消息的服务。这个服务必须是事务型消息并保持原子操作。

使用状态机制对saga编制进行建模

saga编制建模的一个好的方法时状态机制。状态机制由一系列状态和状态之间的转换组成。使用状态机制建模让saga的设计,实现 和测试变得简单。

下面就是创建订单的状态机制。

  • Verifying Consumer:当在该状态下时,saga等待Consumer服务验证Consumer是否可以产生一个订单。
  • Creating ticket:等待创建小票命令的返回。
  • Authorizing Card:等待Accounting 服务授权信用卡。
  • Order Approved:最终状态,表示saga完成成功。
  • Order Rejected:最终状态,订单被某个saga参与者拒绝。

如下图,是saga状态的转换流程图。

基于编制saga的优缺点

优点:

  • 简化依赖:编制器会依赖saga参与者,而参与者不依赖编制器。所以不会引入循环依赖。
  • 解耦:每个服务实现一个供编制器调用的api,不需要了解其他参与者发出的事件。
  • 减少相互间的关联,简化业务逻辑:saga编制逻辑集中在编制器中。

缺点:

  • 业务逻辑中心化,业务逻辑复杂,难以管理的风险。但是我们可以将编织器和其他业务逻辑分开,编制器只有编制这一单一责任。

基于编制的saga除了非常简单的场景外,适用于大多数的场景。

处理隔离性缺失

saga模式的问题就是,缺少了传统数据库事务的隔离性。会导致如下问题:

  • 更新丢失:一个saga覆盖另一个saga的修改。
  • 脏读:一个saga读取了另一个saga正在更新的数据。
  • 不可重复读/幻读:一个saga两个阶段读取的相同数据,但是得到的结果不一样。

处理隔离性缺失的手段

有如下一些处理隔离性缺失或者减少其出错的措施:

  • 语法锁——应用级别的锁
  • 互斥更新——可以按照任何的顺序执行更新,结果都不变(如账户的借和贷)。
  • 悲观策略——对一个saga的步骤重排序,减少业务风险。
  • 版本文件——为每次更新记录一个版本号。
  • by value——

saga的结构

一个saga由三种类型的事务组成:

  • 可补偿事务——有对应的补偿事务的事务,可以用于回滚。
  • 中轴事务——执行到saga成功的事务,会决定整个saga的成功或失败。
  • 重试型事务——在核心事务后面执行的重试事务,保证核心事务成功。

在订单创建中saga中,创建订单,创建小票是可补偿事务,可以回滚。授权信用卡是 中轴事务。如果信用卡授权成功,saga走到最终 的成功。小票创建成功和订单创建成功是重试型事务。

语法锁

在整个应用中,可补偿型事务对每个需要创建和修改的记录设置一个标记,标记表示该记录处于改动状态。这个标记可以用于阻止或者 警告其他相关的事务。*_PENDING状态就是语法锁的最好应用。告诉其他saga已经有一个saga正在修改。

同时也要处理好语法锁,一种方法是让客户端重试,好处是简单,坏处是让客户端变得复杂。另一种方法是让其他访问者阻塞,直到锁 释放。优点是移除了客户端重试的消耗。缺点是应用必须管理锁。可能形成死锁,所以需要实现一个死锁检测的算法来取消一个处在 死锁的saga或者重新执行它。

悲观策略

悲观策略通过重排saga的顺序来减少脏读的风险。如取消订单一般的顺序是:

  1. 增加账户可用额度。
  2. 修改订单状态为取消。
  3. 取消快递。

可以调整为

  1. 修改订单状态为取消。
  2. 取消快递。
  3. 增加账户可用余额。

重排后的减少了账户余额脏读的可能性。

值重读

值重读防止更新丢失。saga在更新一个值时,读取该值保证这个值没有变化,然后再更新它。如果值改变,那么saga停止并可能重启。 类似于乐观锁的机制。

版本文件
互斥更新
by value

在应用中一般会采用一种或者多种方式保证程序的正确性。让我们以Create Order Saga举例说明。

订单服务和创建订单saga的设计

订单服务的设计如下:

服务的业务逻辑由传统的业务逻辑类组成。如OrderService,Order,OrderRepository等。同时也包括了saga编制类。如CreateOrder Saga等。 同时由于订单服务参与了它自己的saga。所以,有一个OrderCommandHandlers适配器处理saga发给OrderServicec的命令消息。Saga编制器 通过一个saga参与者的代理类发送命令消息给参与者,如OrderService Proxy,KitchenService Proxy等,这些参与者的代理类定义了一个 参与者的消息API。

订单服务类

订单服务类,是一个领域服务,被服务的API层调用。提供创建,更新等服务,会调用OrderRepository持久化订单,也会调用 CreateOrderSaga创建saga。其中SagaManager是用于写saga编制和参与者的的框架中的类。在Eventuate Tram Saga中。

其中创建订单(详情可查看源码)步骤如下:

  • 创建订单
  • repository持久化订单
  • 发布领域事件
  • 创建一个CreateOrderSaga。

其中CreateOrderSaga和它关联的类图如下:

创建订单saga的实现

上图中,主要的参与者如下:

  • CreateOrderSaga:定义了saga状态机制的单例类。saga参与者的Proxy指定了消息通道。CreateOrderSaga调用 CreateOrderSagaState创建命令消息,并使用通道发送给saga参与者。
  • CreateOrderSagaState:saga的持久化状态,包含了消息Id和消息详情。用于创建各种命令消息。
  • Saga参与者Proxy:定义了一个saga参与者的消息API。包括命令通道、命令消息类型和回复类型等。
  • Eventuate Tram Saga:一个saga框架,使用领域专用语言(DSL)定义了一个saga的状态机制。它负责管理saga状态并和saga参与者 交换消息。同时也会将saga状态持久化到数据库。
saga

其中创建订单的saga编制代码如下:

public CreateOrderSaga(OrderServiceProxy orderService, ConsumerServiceProxy consumerService, 
        KitchenServiceProxy kitchenService,AccountingServiceProxy accountingService) {
    this.sagaDefinition =
         step()
          .withCompensation(orderService.reject, CreateOrderSagaState::makeRejectOrderCommand)
        .step()
          .invokeParticipant(consumerService.validateOrder, CreateOrderSagaState::makeValidateOrderByConsumerCommand)
        .step()
          .invokeParticipant(kitchenService.create, CreateOrderSagaState::makeCreateTicketCommand)
          .onReply(CreateTicketReply.class, CreateOrderSagaState::handleCreateTicketReply)
          .withCompensation(kitchenService.cancel, CreateOrderSagaState::makeCancelCreateTicketCommand)
        .step()
          .invokeParticipant(accountingService.authorize, CreateOrderSagaState::makeAuthorizeCommand)
        .step()
          .invokeParticipant(kitchenService.confirmCreate, CreateOrderSagaState::makeConfirmCreateTicketCommand)
        .step()
          .invokeParticipant(orderService.approve, CreateOrderSagaState::makeApproveOrderCommand)
        .build();
  }

step(),invokeParticipant(),onReply(),withCompensation()是由saga框架提供的DSL方法。

saga会按照流程协调参与者完成所有的流程。如果一个saga 参与者失败了,saga会执行倒叙执行补偿事务。

代理类

代理类不是必须要的,一个saga可以直接发送消息给参与者,但是代理类由两个好处:1,代理类定义了静态类型的接口,减少saga发送 错误消息给某个服务的次数。2,一个代理类是为服务调用定义的API,让代码更容易理解和测试。

EVENTUATE TRAM SAGA框架

该框架为编制者和参与者双方使用,其架构图如下:

sagaManager用于持久化一个saga实例,并发送saga产生的消息,订阅返回的消息,并调用saga处理返回的消息。SimpleSaga是saga实例的 基本接口。

订单命令处理类

订单服务类同时也参与了它自身的saga。OrderCommandHandlers用于处理由saga发送过来的消息。它的每个方法调用OrderService来更新 一个订单,并生成一个回复消息。