Saga 模式图:编排式与协同式

Saga 模式如何在没有分布式事务的情况下保持跨服务的数据一致:补偿、关键步骤、编排式与协同式,附一张架构图。

· 创始人兼开发者

发布于

Saga 模式图:编排式与协同式

下单会涉及三个服务:库存服务预留库存,支付服务对银行卡进行授权,配送服务创建发货单。在单体应用里,这是一个数据库事务。拆分成各自拥有数据库的服务之后,就不存在横跨三者的事务了,而在服务之间使用两阶段提交,在实践中很少可行。

Saga 模式是标准答案。本文介绍 Saga 的工作方式、编排式与协同式的区别、补偿和关键步骤如何配合,以及 Saga 不会替你解决的隔离性问题。图中展示的是一个编排式的订单 Saga。

什么是 Saga

Saga 是一串本地事务。每一步都在自己服务的数据库中提交,并触发下一步。如果某一步失败,Saga 会执行 补偿事务,撤销已经提交的那些步骤所产生的影响。

这个想法比微服务更早:Hector Garcia-Molina 和 Kenneth Salem 在 1987 年一篇关于长事务的论文中描述了 Saga。这个模式之所以重新流行,是因为服务边界再次制造了同样的问题。

有两个后果决定了其他的一切:

  • 最终一致性。 在第一步和最后一步之间,系统处于其他请求可以看到的中间状态。订单是 PENDING,库存已预留,银行卡已授权但尚未扣款。
  • 补偿不是回滚。 已经发出的邮件收不回来,银行卡上的扣款也不能悄悄撤销。补偿是一个新的业务操作,比如撤销授权或发起退款,在语义上抵消之前的操作。

可补偿、关键和可重试的步骤

动手画图之前,先给每一步分类。Azure 架构中心的 Saga 模式指南 把步骤分为三类:

类型 含义 示例
可补偿 可以用另一个事务撤销 预留库存;授权支付
关键(pivot) 不可回头的点:一旦成功,Saga 必须执行到底 创建发货单
可重试 位于关键步骤之后,最终必须成功;幂等 发送确认邮件

顺序很重要。可补偿的步骤放在最前,然后是关键步骤,之后只放可重试的步骤。关键步骤失败就补偿;它一旦成功,就不断重试之后的所有步骤,直到成功为止。如果关键步骤之后还有可补偿的步骤,那么这一步失败时,Saga 就无法到达一致的状态。

编排式与协同式

协调这些步骤有两种方式。

编排式: 由一个 Saga 编排器(通常是带持久化状态的状态机)告诉每个服务该做什么,并等待回复。顺序、重试、超时和补偿都由它负责。

协同式: 没有协调者。每个服务监听事件并作出反应:库存服务收到 OrderCreated 后预留库存,支付服务收到 StockReserved 后进行授权,依此类推。失败也是事件,每个服务都知道该执行哪个补偿。

编排式 协同式
流程在哪里 一个地方:编排器 分散在各个服务的事件处理器中
增加一个步骤 修改编排器 修改谁监听哪个事件
查看某个订单的状态 查询 Saga 日志 从事件中重建
耦合 服务了解编排器的命令 服务了解彼此的事件
适用场景 步骤多、有分支、顺序严格 两三个步骤的简单流程

协同式在短流程里用起来很舒服,可一旦有人问“为什么订单 8812 卡住了?”,就很头疼。编排式让这个问题只需要看一个地方。Temporal、AWS Step Functions 或 Camunda 这样的工作流引擎提供持久化的编排,你不必自己实现状态机。Chris Richardson 的 Saga 模式页面 给出了两种风格的完整示例。

隔离性:Saga 留给你的部分

数据库事务会对其他事务隐藏中间状态,Saga 不会。订单 Saga 运行期间,另一个请求可能读到已预留的库存,第二个 Saga 也可能修改同一条记录。常见的应对措施如下:

  • 语义锁。 把记录标记为处理中(PENDING 的订单、RESERVED 的库存),让其他操作知道应该等待或拒绝。
  • 可交换的更新。 把操作设计成以任意顺序执行都得到相同结果,比如增减计数,而不是直接设置数值。
  • 写入前重新读取。 检查某个值在 Saga 读取之后是否发生了变化,如果变了,就失败或重新开始。

让每一步和每个补偿都幂等,以 Saga ID 和步骤作为键。消息会被投递两次,而一次重试的“撤销授权”绝不能撤销另一笔支付。

超时与卡住的 Saga

每一步都需要截止时间。一个永远不回复的支付服务,会让 Saga 永远等下去,库存也一直处于预留状态。给每一步设置超时:在关键步骤之前,把超时当作失败(进行补偿);在关键步骤之后,把超时当作重试的理由;并对在某个状态停留时间超出预期的 Saga 发出告警。一个按当前步骤列出 Saga 的小仪表盘,本身就能回答大部分支持问题。同时记录每个 Saga 结束的原因。

架构图

订单服务在编排器中启动一个 Saga,编排器记录每一步,并依次调用库存、支付和配送服务。一个判断节点检查所有步骤是否成功:是则订单获批,否则按相反顺序补偿,订单被拒绝。

11 个组件 · 9 个连接

在 Archboard 中打开
图表内容
  • Order service
  • Saga orchestrator
  • Saga log
  • Inventory service
  • Payment service
  • Shipping service
  • All steps succeeded?
  • Order APPROVED
  • Compensate in reverse order
  • Order REJECTED
  • start
  • persist each step
  • 1. reserve stock
  • 2. authorize payment
  • 3. create shipment
  • yes
  • no
  • void, release
由编排器驱动的订单 Saga。它以本地事务的方式预留库存、授权支付并创建发货单,持久化每一步;如果关键步骤之前的某一步失败,就按相反顺序补偿之前的步骤。

这张图的读法如下:

  • 左侧: 订单服务以 PENDING 状态创建订单,并启动 Saga。
  • 中间: 编排器按顺序执行各步骤,并把每一步持久化到 Saga 日志中,因此重启后可以从中断的地方继续。
  • 右侧: 三个服务,各自标注了步骤编号和类型。库存和支付是可补偿的,配送是关键步骤。
  • 底部: 如果所有步骤都成功,订单变为 APPROVED。如果关键步骤之前的某一步失败,编排器按相反顺序补偿(先撤销授权,再释放库存),订单变为 REJECTED。
  • Markdown 表格 把每个操作与它的补偿一一对应,这是评审者首先应该检查的内容。

Saga 图上应该画什么

  • 步骤顺序和每一步的类型。 可补偿、关键还是可重试,要写在方框上。
  • 每一个补偿。 没有补偿的步骤,要么是关键步骤,要么可重试,要么就是 bug。
  • Saga 状态保存在哪里。 中途崩溃就无法恢复的 Saga,还不算完成。
  • 可见的中间状态。 PENDING 和 RESERVED 是设计的一部分,其他功能会看到它们。

Saga 与 事务性发件箱模式 天然搭配,因为每一步都必须既更新数据库又发送消息,而且两者都不能丢。微服务架构图 展示了这些服务所处的位置,支付网关集成图 则详细介绍了授权和请款这两个步骤。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 添加 C4 模型节点包中的 容器 节点,分别表示订单服务、Saga 编排器和三个参与服务,再添加一个 数据库 作为 Saga 日志。
  3. 在每个服务的技术字段中写上步骤编号和类型(可补偿或关键)。
  4. 添加流程图节点包中的一个 判断、一个 处理 和两个 开始 / 结束 节点,表示成功路径和补偿路径。
  5. 添加一个基础节点包中的 Markdown 块,用表格列出各个操作及其补偿。

这个示例共用 11 个图表节点,在 Free 套餐的限制之内。

常见问题

Saga 和分布式事务有什么区别? 分布式事务(两阶段提交)会锁住多个数据库中的资源,直到所有参与方都提交。Saga 在本地提交每一步,如果后面的步骤失败,就用补偿撤销前面的步骤,用隔离性换取可用性。

应该用编排式还是协同式? 步骤多、有分支或顺序严格的流程用编排式,因为每个 Saga 的状态都在一个地方。两三个服务彼此响应事件的短流程用协同式。

什么是关键事务? 在这一步之后,Saga 就无法再被补偿。它之前的所有步骤都必须可补偿;它之后的所有步骤都必须可以重试,直到成功。

补偿失败了怎么办? 重试。补偿必须幂等,并且最终成功;如果某个补偿无法成功,Saga 就需要一条人工处理的路径和一个告警。

Saga 能提供隔离性吗? 不能。其他请求可以看到中间状态。使用 PENDING 状态这样的语义锁、可交换的更新和重新读取,来防止对你的业务真正有影响的异常。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。