在微服务架构中使用 saga 设计模式

在单体应用中,我们通常将数据库事务与 (原子性、一致性、隔离性、持久性)属性。 在回滚的情况下,很容易确保开箱即用的数据一致性。 但是现在我们的数据库事务分布在多个微服务中,我们怎样确保它们之间的数据一致性呢?

一种解决方案是 saga 模式——一种较旧的架构概念,今天仍然与微服务高度相关。 Saga 模式为我们的系统带来了希望,因此我想分享一些我们一直在探索在我们自己的微服务中使用 saga 的关键见解。

传奇模式

saga 是每个参与的微服务中的一系列本地事务。 它有自己必须执行的步骤,当每个步骤完成时,都有某种逻辑来决定下一步该做什么。

saga 必须保证所有步骤都成功执行,否则它必须在必要时执行回滚。

下图说明了微服务之间的请求流,以完成创建订单传奇。

发出请求时可能会出现基础架构或业务逻辑规则异常。 所有这些异常都应该被处理,如果我们在特定步骤中有异常,我们还必须撤消之前步骤中的所有更改。

有时,要进行完全回滚,我们必须向微服务发出额外的请求。 这些额外的请求被称为补偿交易。

所有这些都意味着使用 sagas 会增加复杂性。 因为我们自己实现了回滚机制,所以应该仔细考虑每个场景。

两种实现方式

有两种方法可以实现 saga 模式,每种方法都有不同的方法来协调工作流。

  • 编排 (集中):一个中央协调器负责调用远程服务来完成saga;
  • 编舞 (分布式):没有中央协调器,因此每个服务都应该监听并产生事件并决定需要执行哪些操作。

让我们更深入地了解这些实现方法。

1. 编排

编排方法是当我们有一个集中的协调器来管理所有逻辑并知道何时与其他微服务通信以及下一步该做什么或怎样回滚时。

使用单独的微服务进行协调是一种很好的做法。

当 saga 的逻辑由一两个团队维护时,编排方法效果最好。 否则,如果没有任何人负责代码质量和维护,代码可能会变得复杂。

另一方面,编排方法更适合理解更复杂的工作流程。 从架构的角度来看,这也是一种更简洁的方法,因为微服务之间没有相互耦合。

编排中的回滚

协调者应该知道在失败的情况下怎样回滚。 出于这个原因,协调器必须为每个流存储一个事件日志,并在执行回滚时在每个相应的微服务中执行补偿事务。

如果任何请求中发生任何故障,事件日志将帮助协调器确定哪些微服务受到影响以及补偿事务应按什么顺序发送。

显示失败的 saga 模式编排图

如上图所示,付款处理请求失败,订单被拒绝并退款。 我们也跳过了向客户发送发票。 或者,可以发送带有付款失败信息的发票。

这个图只是一个 example. 实际上,可以对这个过程进行一些改进,但这 example 使过程更容易理解。

2. 编舞

这种类型的 saga 是基于事件/异步的,并且在每个服务中运行的代码决定怎样处理其范围内的事件以及下一步做什么时实现。

您也可以将其视为与事件相关的一系列微服务。 每个服务都侦听其他事件并发布自己的事件,但不幸的是它会导致彼此耦合。

saga 模式编排图

当有更多团队参与 sagas 的管理时,这种方法是最好的。 好处是每个团队都可以在自己的范围内专门研究 sagas。

因为没有中央协调器,我们不需要一个单独的微服务来负责协调工作流。 但是,这可能会导致难以理解更复杂的工作流程以及某些服务怎样相互关联。 另外,请记住,微服务之间可能存在循环依赖。

编排中的回滚

由于没有集中协调器,因此相应的微服务会监听故障事件以便能够回滚。

显示失败的 saga 模式编排图

在上图中,Warehouse API 未能保留库存并发布相应的“Reservation Failed”事件。 然后其他服务响应此事件——订单 API 拒绝订单,支付 API 退还付款。

混合风格

尽管这两种类型的传奇非常不同,但它们可以混合在一起。 当一个 saga 有一个协调器但有一个事务(对于 example,保留库存)可以编排。

其他传奇提示

  • 全局事务 ID: 给 sagas 全局事务 ID 是个好主意。 它们可以帮助监控或调试,但在所有事件中共享全局事务 ID 可以让您识别哪个事务失败并采取适当的措施。
  • 语义锁:在每一步使用 saga 机制时,都会将更改提交给单独的微服务。 saga 哲学不允许我们使用真正的锁定,这可能会在 saga 仍在进行时导致数据不一致。

    您可以包含临时状态值,以防止其他事务更改错误的值。 让我们使用 PENDING 状态来指示订单正在处理中。 现在,如果订单状态为 PENDING,我们可以防止取消订单,其他微服务也将能够读取 PENDING 状态。

    这意味着我们需要特别注意可能无法满足 CID 隔离属性的情况,并确保我们对其进行维护。

  • 幂等性: 幂等操作有助于避免在两次发送相同请求时出现问题,特别是对于正在进行回滚时的补偿事务。
  • 返回响应: Sagas 有时可以包含许多可能需要一段时间才能完成的事务。 这就提出了一个问题:我们应该在所有这些事务完成后返回响应还是让它们的工作异步?

    对于更长的 saga,立即返回响应并异步处理 saga 事务变得至关重要。 我强烈推荐这种方法。 它遵循 REST API 的最佳实践。 确保所有请求都很小并且可以快速执行。

    在这些情况下,我们通常会返回 http 状态 202,表示“已接受”。 然后客户端可以使用相应的端点检查结果,或者应该通过 API 服务通知该 saga 已执行。

概括

我们已经研究了几种实现 saga 模式的方法及其优缺点。

一般来说,如果您需要带有事务补偿的回滚流程,或者您正在处理长期事务,那么 sagas 是一个不错的选择,甚至是您的最佳选择。

尽管 saga 模式引入了更大的复杂性,但当您需要在没有紧密耦合的情况下管理微服务之间的数据一致性时,它更可取。

订阅订阅您已成功订阅我们的时事通讯!电子邮件无效订阅