Dawn's Blogs

分享技术 记录成长

0%

微服务架构设计模式 (4) 使用Saga管理事务

Saga 分布式事务

Saga 是一种分布式事务实现方式,Saga 通过异步消息来协调一系列的本地事务,从而维护多个服务之间的数据一致性。

异步消息的一个重要好处就是确保 Saga 的所有步骤都被执行,即使一个或者多个 Saga 的参与方暂时不可用。

Saga 缺少了 ACID 事务中的隔离性。此外,因为每个本地事务都提交了更改,必须使用补偿事务(只有修改操作有补偿事务)回滚 Saga。

在 Saga 的一系列的步骤中,若前 N 个步骤后面跟着的步骤可能失败,称为可补偿性事务。第 N+1 个步骤后面全部都是不可能失败的步骤,称 N+1 步骤为关键性事务。关键性之后的步骤称之为可重复性事务

Saga 的协调模式

Saga 由一些列本地事务组成,如何协调 Saga 中一系列的本地事务进行下一步操作还是执行补偿事务尤为关键,总共有两种方式:

  • 协同式:把 Saga 的决策和执行顺序分布在 Saga 的每一个参与方中,它们通过交换事件的方式进行沟通。
  • 编排式:将 Saga 的决策和执行顺序集中在一个集中的编排器中,编排器发送消息给各个Saga 参与方。

协同式

在协同式的 Saga 中,参与 Saga 的每一个服务都独自进行决策并且决定下一个步骤的执行顺序,Saga 的参与方通过发布/订阅的方式进行交互。在实现基于协同的 Saga 时,必须考虑服务间通信的相关问题:

  • 确保更新数据库和发布消息作为数据库事务的一部分,在更新数据库数据成功的同时必须保证发送消息也同样成功,所以需要采用事务性消息
  • 确保收到的消息属于哪一个 Saga 事务,可以让 Saga 参与方发布包含相关性 ID 的事件,这个相关性 ID 使得其他参与方确定这个消息属于 Saga 事务。如在创建账单的 Saga 事务中,账单 ID 就可以作为相关性 ID。

协同式 Saga 的好处在于:

  • 简单:没有一个集中的控制器去控制 Saga 的流程,结构简单。

弊端在于:

  • 难以理解:Saga 事务被分布在各个服务的实现当中,因为不在一个单一的地方定义,开发人员难以理解 Saga 事务的流程以及如何工作。
  • 服务之间的循环依赖:Saga 参与方订阅事件,可能会导致循环依赖。
  • 紧耦合的风险:每一个 Saga 参与方都需要订阅所有影响自己的事件。消息的订阅方和发布方需要在关于实现 Saga 的事件方面,应该是紧耦合的。

通常使用编排式 Saga 事务。

编排式

在编排式 Saga 中,定义一个事务编排器用于管理 Saga 事务。编排器使用命令/异步响应的方式与 Saga 参与方进行通信。在每一个步骤中,Saga 编排器会给参与方发送命令式的消息,告诉参与方该做什么工作。当参与方完成曹州,会给编排器答复。编排器根据这个答复,决定下一步的 Saga 操作是什么。

在编排式的 Saga 事务中,每一步骤也包括更新数据库和发布消息。和协同式一样,需要保证在更新数据库数据成功的同时必须保证发送消息也同样成功,所以需要采用事务性消息

编排式 Saga 的好处

  • 更简单的依赖关系:不会引入循环依赖关系。Saga 编排器会调用 Saga 参与方,但是参与方不会调用编排器。
  • 较少的耦合:每一个服务实现供编排器调用的 API,不需要知道其他参与方发布的事件。
  • 简化业务逻辑:不需要了解服务参与的 Saga 流程。

弊端在于:

  • 编排器可能会存在过多的业务逻辑。

解决隔离问题

Saga 事务不满足 ACID 中的隔离性要求,因为本地事务一旦提交,其更改就会被其他事务看到。其他 Saga 可能在执行时更改该 Saga 所访问的数据,也可以在 Saga 完成之前读取到数据,这可能会导致不一致。

可以使用以下对策来解决隔离问题。

语义锁

在数据库表中设置一个标记,表示这一条记录未提交。这个标志可以阻止其他事务访问记录,相当于行锁。标志会被一个可重复性事务清除,表示 Saga 成功完成;也可以通过补偿性事务清除,表示 Saga 发生了回滚。

当遇到语义锁时,可以直接返回失败,告诉客户端稍后再尝试(增加了客户端的复杂度,客户端需要加上重试机制);也可以阻塞当前 Saga,当释放了语义锁之后再执行当前 Saga(增加了服务的复杂度,必须管理语义锁,检查语义锁是否死锁)。

交换式更新

将更新操作设计为可交换的(可以按任意的顺序执行),比如对于更新操作 a = a+1 就是可交换的,而 a=2 就是不可交换的。这样就避免了丢失更新(更新被覆盖)的问题,但是脏读问题无法避免。

简而言之,自加、自减、自除、自乘操作就是可交换的,而直接赋值的更新操作就是不可交换的。

悲观视图

重新排序 Saga 步骤,以最大限度地降低由于脏读而导致的业务风险。

重读值

重读值是一种乐观锁策略,在进行操作之前重新读取记录,验证是否更改。如果记录已经更改,则终止 Saga 事务(可能重新启动)。

版本文件

记录了对数据执行的操作,以便对它们重新进行排序。这是将不可交换操作转换为可交换操作的一种方法。

如有这样一个场景,信用卡授权服务负责用户的信用卡授权工作。对于信用卡授权、信用卡取消授权而言,这是不可交换的操作。但是信用卡授权服务可以在请求到达时记录操作,然后以正确的顺序执行。比如对同一个信用卡的取消授权在授权操作之前到达,会首先记录授权操作,当收到授权操作请求时,会注意到已经进行了取消授权操作,所以会跳过信用卡的授权。