微服务架构设计模式-第四章
第4章 使用Saga管理事务
它的主要内容是提出了“Sagas”(传奇)这个事务模型,核心是为了解决长事务(Long-running transactions)在分布式系统中遇到的难题。
为了解决长事务容易长时间占用资源、失败后恢复代价高昂的问题,Sagas模型的核心思想是:
将长事务拆分成一系列子事务:每个子事务都是可以独立提交的短事务。
引入补偿机制:为每个子事务定义一个对应的“补偿操作”(Compensating Operation)。如果整个Saga流程需要回滚,系统不会像传统数据库那样直接撤销数据,而是依次执行这些补偿操作来抵消之前子事务的影响。
简单来说,这是一种通过牺牲强一致性,来换取更高可用性和性能的分布式事务处理思路,对后来微服务架构中的分布式事务设计影响深远。
4.1 微服务架构下的事务管理
https://en.wikipedia.org/wiki/X/Open_XA
X/Open XA 是一个由 X/Open 组织(现为 The Open Group)定义的、用于分布式事务处理的行业标准规范。它主要定义了一个资源管理器(如数据库)与事务管理器之间的双向通信接口。
以下是它的几个关键要点:
1.核心目的:XA 标准旨在允许多个不同的资源(例如,两个不同的数据库,或一个数据库和一个消息队列)在同一个全局事务中协同工作,并保证所有操作要么全部成功提交,要么全部失败回滚,从而维持数据一致性。
2.主要角色:
应用程序 (AP):发起和结束事务的应用程序。
事务管理器 (TM):如交易中间件或应用服务器,负责全局事务的协调、发起提交或回滚的指令。
资源管理器 (RM):如数据库系统,负责实际管理数据资源,并响应事务管理器的指令。
3.两阶段提交 (2PC):XA 规范的核心实现机制通常基于两阶段提交协议。
第一阶段(准备阶段):事务管理器询问所有参与的资源管理器“是否可以提交?”。每个资源管理器执行操作并记录日志,然后回复“准备好”或“失败”。
第二阶段(提交/回滚阶段):如果所有资源都回复“准备好”,事务管理器则广播“提交”指令;如果任一资源回复“失败”,则广播“回滚”指令。所有资源管理器执行最终操作。
4.优点与局限:
优点:提供了强大的数据一致性保证,是金融等强一致性场景的经典解决方案。
局限:由于同步阻塞和多次网络通信,性能开销较大;此外,在极端故障情况下(如事务管理器宕机),可能仍存在短暂的不一致窗口。它通常不适合需要高并发和快速响应的现代微服务架构。
4.1.1 微服务架构对分布式事务的需求
单体:createOrder()操作必须验证消费者是否满足下订单的相关条件、验证订单内容、完成消费者的信用卡授权,以及在数据库中创建 Order。在验证中需要的所有数据都可以从数据库中直接读取,此外,可以使用一个 ACID 类事务来保证数据的一致性。只需要在 createOrder()服务方法之前使用一个@TransactionalSpring注解即可。
微服务:验证的数据在不同的服务中,createOrder()必须访问多个服务(包括:consumer、kitchen、accounting)来获得它所需要的验证数据。
4.1.2 分布式事务的挑战
注意:XA,应用程序的整个技术栈需要满足 XA 标准,包括符合 XA 要求的数据库、消息代理、数据库驱动、消息 API,以及用来传播 XA 全局事务 ID 的进程间通信机制。
分布式事务并不像听起来这么简单,而是有着许多问题。其中一个问题是,许多新技术,包括 nosql,并不支持 XA 标准的分布式事务。同样,一些流行的消息代理如 rabbitmq 和 kafka 也不支持分布式事务。
另一个问题是,它们本质上都是同步进程间通信,这会降低分布式系统的可用性。为了让一个分布式事务完成提交,所有参与事务的服务都必须可用。分布式事务每增加一个事务参与方,都会进一步降低总体的可用性。CAP
4.1.3 使用Saga模式维护数据一致性
saga 由一连串的本地事务组成。每一个本地事务负责更新它所在服务的私有数据库,这些操作仍依赖于我们所熟悉的 ACID 事务框架和函数库。
saga 使用补偿事务回滚所做出的改变
如果 createOrder 的第四步失败,则 FTGO 应用程序必须明确撤销前三个步骤所做的更改。你必须编写所谓的补偿事务。
| 步骤 | 服务 | 事务 | 补偿事务 |
|---|---|---|---|
| 1 | order service | createOrder() | rejectOrder() |
| 2 | consumer service | verifyConsumerDetail() | - |
| 3 | kitchen service | createTicket() | rejectTicket() |
| 4 | accounting service | authorizeCreditCard() | - |
| 5 | kitchen service | approveTicket() | - |
| 6 | order service | approveOrder() | - |
createOrder的前三个步骤,被称为可补偿性事务,因为他们后面跟着的步骤可能失败;第四个步骤称为关键性事务(pivot transaction),因为它后面跟着不可能失败的步骤;最后两个步骤被称为可重复性事务,因为它们总是会成功。
如果消费者信用卡授权失败,在这种情况下,saga 执行以下本地事务:
- Order service:创建一个处于 approval_pending 状态的 order
- consumer service:验证消费者是否可以下订单
- kitchen service:验证订单内容,并创建一个后厨工单 ticket,状态为 create_pending
- accounting service:授权消费者的信用卡,但失败了
- kitchen service:将后厨工单 ticket 的状态更改为 create_rejected
- order_service:将 order的状态更改为 rejected
是否需要补偿,完全取决于该事务操作是否对系统产生了“写”的影响。 只读的校验、查询等操作,从一开始就天然地游离在补偿机制之外。
在Saga模式中,“关键性事务”通常指的是整个流程的最后一步或最核心的成功标志。它一旦成功,整个分布式事务就宣告完成。
- 它本身通常不需要补偿,因为它的成功意味着整个业务目标已经达成。
- 它的失败则意味着整个流程无法继续,通常会导致Saga终止并触发所有之前成功步骤的补偿操作,以回滚到初始状态。
一个完整的业务流程可能包含多个关键事务,比如:
订单确认(订单生成)确立了交易的法律和业务实体,生成了唯一的交易凭证(订单号)。
资金结算(支付成功)完成了资金流的转移,是业务收入确认的关键点,也是后续履约的起点。
履约交付(发货完成)标志着物流/服务流的正式启动,意味着商家已履行核心交付义务。
服务完成(确认收货)标志着整个交易闭环的最终完成,通常也是资金最终结算给商家的节点。
当一个流程有多个关键事务时,它会带来几个重要的设计和运维考量:
- 流程被划分为多个“安全阶段”:每个关键事务都是一个阶段的终点和下一个阶段的起点。流程被打散成更小的、可管理的片段,降低了单次操作的风险。
- 故障恢复策略不同:对于关键事务本身,我们一般不设计补偿操作(不撤回,而是向前修正)。例如,支付成功后不会去“回滚”支付,而是如果后续发货失败,会发起“退款”这个新流程来处理,而不是回滚整个交易。
- 需要更精细的状态机管理:订单的状态(如待支付、已支付、已发货)本质上就是由这些关键事务驱动的。系统需要有一个清晰的状态机来管理这些状态的流转,并处理各个状态下可能出现的异常。
- 最终一致性的要求更高:在分布式环境下,这些关键事务可能由不同的微服务处理(订单服务、支付服务、物流服务)。系统需要确保,即使某一步暂时失败,最终也能通过重试、异步补偿或人工介入等方式,达到一个业务上可接受的最终状态(如订单完成或全额退款)。
4.2 Saga的协调模式
是的,无论使用哪种Saga实现框架,事务补偿逻辑(即回滚操作)都需要你自己来编写。
这是由Saga模式本身的特性决定的——框架只能负责协调和调度,但无法自动生成业务层面的补偿操作。具体来看:
为什么补偿逻辑必须自己写?
Saga模式的核心是“通过补偿来反向操作”,但“如何补偿”完全是业务相关的。例如:
- 订单服务:创建订单的补偿操作是“取消订单”,更新库存的补偿是“恢复库存”。
- 支付服务:扣款的补偿操作是“发起退款”。
- 预订服务:预订座位的补偿是“释放座位”。
这些操作涉及具体的业务逻辑、状态检查和权限校验,框架无法自动生成。因此,你需要为Saga流程中的每一个正向操作,显式地定义一个对应的补偿操作。
不同实现方式下的编码模式
虽然都需要自己写补偿,但不同框架的编码方式略有不同:
| 实现方式 | 你只需要做的事 | 框架负责做的事 |
|---|---|---|
| 注解式框架(如 ServiceComb Saga) | 1. 编写正常业务方法(如createOrder)2. 编写补偿方法(如 cancelOrder)3. 在正常方法上添加类似 @SagaStart、@Compensable(compensationMethod = "cancelOrder") 等注解。 |
在事务执行过程中,如果某个步骤失败,自动回调你指定的补偿方法。 |
| 状态机/编排框架(如 Seata Saga、Conductor) | 1. 将正常业务和补偿业务都封装成独立服务。 2. 在状态机定义文件(如JSON或DSL)中,显式地为每个状态(步骤)配置其对应的补偿状态。 |
按照状态机定义执行流程,当出现异常时,自动驱动流程跳转到对应的补偿状态执行。 |
| 事件驱动/自定义实现(基于MQ) | 1. 编写服务,发送“业务完成”事件。 2. 编写独立的“监听器”来消费这些事件,并自行判断:如果后续步骤失败,则发布“业务回滚”事件。 3. 完全手动管理事务状态、重试和幂等。 |
基本不提供Saga支持,只提供消息传递能力。你需要自己构建整个调度和补偿的触发机制。 |
编写补偿逻辑的通用原则
无论用哪种框架,编写补偿操作时,建议遵循以下几点:
- 补偿操作必须实现幂等性:由于网络或系统故障,补偿操作可能被多次调用,因此必须保证执行多次的结果与执行一次相同(例如,使用状态标识“已取消”来防止重复退款)。
- 补偿操作应当是“逻辑补偿”:通常是对数据进行更新或标记状态,而不是直接物理删除数据。例如,用“状态=已取消”来补偿“状态=已创建”,而非直接删除订单记录,以便保留审计历史。
- 正向操作与补偿操作应当解耦:补偿方法应设计为独立的业务服务,不依赖正向操作的本地变量或上下文,因为正向操作可能已经失败或回滚。
总而言之,Saga框架帮你解决了“何时触发补偿”和“如何调度补偿”的难题,但“补偿什么”这个核心业务逻辑,必须由最了解业务的人(也就是开发者你)来亲手编写。
seata saga
Seata Saga 的实现方式很特别,它的核心是基于状态机引擎来编排整个分布式事务流程的。这意味着,你需要通过一个定义好的状态图,而不是编写复杂的代码,来告诉 Seata 你的业务步骤和补偿规则。
它的实现思路可以概括为三个层面。
第一个层面是核心思想,也就是通过状态图定义业务。你首先需要把整个业务流程画成一个状态图,并生成一份 JSON 格式的状态语言定义文件。这个图中的每个节点都代表一个需要执行的服务调用,例如扣减库存、创建订单。最关键的是,每个正向服务节点都需要配置一个对应的补偿节点,用于定义当这个步骤失败时,该如何撤销它的影响。
第二个层面是执行机制,采用事件驱动和状态流转的方式。这个 JSON 文件会被 Seata 的状态机引擎加载并驱动执行。整个执行过程是事件驱动的:一个节点执行完成后,会产生一个路由消息放入事件队列。事件消费端会从这个队列中取出消息,然后根据状态图定义,决定下一步要执行的节点。以此类推,直到整个流程执行完毕或出现异常。
第三个层面是异常与补偿。当流程中任何一个节点执行失败时,Seata 的状态机引擎会自动开启补偿模式。它会反向遍历这个流程中所有已经成功执行的节点,然后根据这些成功节点在 JSON 里配置的补偿状态,去调用对应的补偿服务,执行回滚操作。
除此之外,Seata Saga 还非常注重状态与日志管理。在整个状态机启动时,它会向 Seata Server 发起请求,开启一个全局分布式事务并生成全局事务 ID。执行每个状态节点时,也会向 Seata Server 注册分支事务。为了支持故障恢复,状态机引擎会将执行的每一个关键事件都持久化到业务数据库中。状态机引擎本身是无状态的,内嵌在业务应用中。如果某个应用实例宕机,Seata Server 会感知到,并将事务恢复请求发送给其他存活的实例,该实例会从数据库加载日志,恢复状态机的上下文并继续执行,从而实现高可用。
下面用一个经典的电商下单场景来举例,这个场景包含创建订单、扣减账户和扣减库存三个步骤。对应的 JSON 状态图大致会是这样:
首先是一个名为 reduceInventoryAndBalance 的状态机,备注是“在一个事务中先扣减库存,再扣减余额”,起始状态是 ReduceInventory。
在 States 部分,第一个状态是 ReduceInventory,类型是 ServiceTask,服务名称是 inventoryAction,调用方法是 reduce,它指定的补偿状态是 CompensateReduceInventory,执行成功后的下一个状态是 ReduceBalance。它的输入参数来自 orderRequest,状态判断逻辑是:如果返回结果为 true 则判定为成功(SU),false 则判定为失败(FA),如果抛出异常则判定为未知(UN)。
第二个状态是 ReduceBalance,类型同样是 ServiceTask,服务名称是 accountAction,调用方法是 reduce,补偿状态是 CompensateReduceBalance,执行成功后的下一个状态是 Succeed。它的输入和状态判断逻辑与上一个状态相同。
接下来是两个补偿状态的定义。CompensateReduceInventory 类型是 ServiceTask,服务名称是 inventoryAction,调用补偿方法 compensateReduce,输入同样来自 orderRequest。CompensateReduceBalance 类型也是 ServiceTask,服务名称是 accountAction,调用补偿方法 compensateReduce,输入也来自 orderRequest。
最后是一个成功状态 Succeed,类型是 Succeed,表示整个流程正常结束。
从这个例子可以看出,你主要的工作就是定义 JSON 文件,并在其中用 CompensateState 属性将正向服务和补偿服务关联起来,用 Next 属性定义成功后的下一个状态,用 Status 属性定义如何判断服务执行结果是成功、失败还是未知。
Seata 选择状态机加 DSL 的方案,主要是基于以下优势。第一,支持服务编排,适合复杂的、包含多个分支的长业务流程,并能满足事务最终一致性的要求。第二,提高吞吐量,基于事件驱动可以更好地利用异步处理来提高系统整体吞吐量。第三,强大的恢复能力,通过持久化的日志,状态机可以实现宕机后的向前重试或向后补偿,这是纯注解方式难以做到的。
4.2.1 协同式Saga
(choreography):把 saga 的决策和执行顺序逻辑分布在 saga 的每一个参与方中,它们通过交换事件的方式来进行沟通,没有一个中央协调器会告诉 saga 参与方该做什么。参与方订阅彼此的时间并做出相应的响应。

Saga的正常工作路径如下所示:
1.Order Service 创建-个处于APPROVAL_PENDING状态的 Order并发布 Order-Created事件。
2.Consumer Service消费OrderCreated事件,验证消费者是否可以下订单,并发布 ConsumerVerified事件。
3.Kitchen Service 消费OrderCreated事件, 验 证Order, 创建一个处于CREATE PENDING状态的后厨工单Ticket,并发布 TicketCreated事件。
4.Accounting Service 消费 OrderCreated事件并创建一个处于 PENDING状态的 CreditCardAuthorization.
5.Accounting Service 消费 TicketCreated 和 ConsumerVerified 事件, 向消费者的信用卡收费,并发布CreditCardAuthorized事件。
6.Kitchen Service 消费 CreditCardAuthorized 事件并将 Ticket 的状态更改为 AWAITING ACCEPTANCE.
7.Order Service 接收 CreditCardAuthorized事件, 将Order 的状态更改为APPROvED,并发布OrderApproved事件。

1.Order Service 创建-个处于APPROVAL_PENDING状态的 Order并发布 Order-Created事件。
2.Consumer Service消费OrderCreated事件,验证消费者是否可以下订单,并发布 ConsumerVerified事件。
3.Kitchen Service 消费OrderCreated事件, 验证Order, 创建一个处于CREATE PENDING状态的后厨工单Ticket,并发布 TicketCreated事件。
4.Accounting Service 消费 OrderCreated 事件并创建一个处于 PENDING状态的 CreditCardAuthorization
5.Account Service 消费 TicketCreated 和 ConsumerVerified事件, 向消费者的信用卡扣款(失败了),并发布 CreditCardAuthorizationFailed事件。
6.Kitchen Service 消费 CreditCardAuthorizationFailed 事件, 然后把后厨工单Ticket的状态更改为REJECTED。
7.Order Service 消费 CreditCardAuthorizationFailed 事件, 并将 Order的状态更改为 REJECTED。
saga 实现基于发布/订阅的通信时需要考虑的一些问题。
可靠的事件通信
确保 saga 参与方将更新本地数据库和发布事件作为数据库事务的一部分(参考第三章:事务性消息)
saga 参与方必须能够将接收的每个事件映射到自己的数据上。例如,当Order Service 收到CreditCardAuthorized事件时,它必须能够查找相应的Order。解决方案是让Saga参与方发布包含相关性ID的事件,该相关性ID使其他参与方能够执行数据的操作。(orderId)
好处:
- 简单:服务在创建、更新或删除业务对象时发布事件
- 松耦合:参与方订阅事件并且彼此之间不会因此而产生耦合
弊端:
- 更难理解:没有一个单一地方定义了 saga
- 服务之间的循环依赖关系:参与方订阅彼此的事件,通常会导致循环依赖关系
- 紧耦合:每个参与方都需要订阅所有影响它们的事件
4.2.2 编排式Saga
(orchestration):把 saga 的决策和执行顺序逻辑集中在一个 saga 编排器类中。saga 编排器发出命令式消息给各个 saga 参与方,指示这些参与方服务完成具体操作,当参与方服务完成操作后,会给编排器发送一个答复消息。编排器处理这个消息,并决定 saga 的下一步操作是什么。(本地事务)

Order Service首先创建(实例化)一个Order对象和一个Create Order Saga编排器对象。一切正常情况下的流程如下所示:
- Saga编排器向Consumer Service发送Verify Consumer命令。
- Consumer Service回复Consumer Verified消息。
- Saga编排器向Kitchen Service发送Create Ticket命令。
- Kitchen Service回复Ticket Created消息。
- Saga编排器向Accounting Service发送Authorize Card消息。
- Accounting Service使用Card Authorized消息回复。
- Saga编排器向Kitchen Service发送Approve Ticket命令。
- Saga编排器向Order Service发送Approve Order命令。
注意:create order saga 是 order service 的一个组件。原则上,saga 可以通过直接更新 order 来批准订单。但为了一致性,saga 将 order service 视为另一个参与方
把 saga 编排器视为一个状态机
状态机是建模 saga 编排器的一个好方法。状态机由一组状态和一组由事件触发的状态之间的转换组成。每个转换都可以有一个动作,对 saga 来说动作就是对某个参与方的调用。状态之间的转换由 saga 参与方执行的本地事务完成触发。当前状态和本地事务的特定结果决定了状态转换以及执行的动作(如果有的话)。对状态机也有有效的测试策略。因此,使用状态机模型可以更轻松地设计、实现和测试 saga。

- Verifying Consumer:初始状态。当处于此状态时,该Saga 正在等待Consumer Service验证消费者是否可以下订单。
- Creating Ticket:该Saga正在等待对Create Ticket命令的回复。
- Authorizing Card:等待 Accounting Service 授权消费者的信用卡。
- Order Approved:最终状态,表示该Saga 已成功完成。
- Order Rejected:最终状态,表示Order被其中一个参与方拒绝。
好处:
- 更简单的依赖关系:不会引入循环依赖
- 较少的耦合:每个服务实现供编排器调用的 API,因此它不需要知道 saga 参与方发布的事件。
- 改善关注点隔离、简化业务逻辑:saga 的协调逻辑本地化在 saga 编排器中。领域对象更简单,并且不需要了解它们参与的 saga。
弊端:
- 在编排器中存在集中过多业务逻辑的风险
4.3 解决隔离问题
一旦该事务提交,每个 saga 的本地事务所做的更新都会立即被其他 saga 看到。
- 其他 saga 可以在执行时更改该 saga 所访问的数据
- 其他 saga 可以在 saga 完成更新之前读取其数据
4.3.1 缺乏隔离导致的问题
- 丢失更新
- 脏读
- 模糊或不可重复读
4.3.2 Saga模式下实现隔离的对策
order 使用 *_pending 状态告诉其他事务,该 order 正在被一个 saga 更新,请进行相应处理(比如等待 saga 完成)
1998 Semantic ACID properties in multidatabases using remote procedure calls and update propagations
论文中描述的对策如下:
- 语义锁:应用程序级的锁
- 交换式更新:把更新操作设计成可以按任何顺序执行
- 悲观视图:重新排序 saga 的步骤,以最大限度地降低业务风险
- 重读值:通过重写数据来防止脏写,以在覆盖数据之前验证它是否保持不变
- 版本文件:将更新记录下来,以便可以对它们进行排序
- 业务风险评级(by value):使用每个请求的业务风险来动态选择并发机制
一个 saga 由三种不同类型的事务组成:可补偿性事务(可以回滚,因此有一个补偿事务);关键性事务(这是 saga 的成败关键点);可重复性事务,它不需要回滚并保证能够完成
对策:语义锁
saga 的可补偿性事务会在其创建或更新的任何记录中设置标志。该标志表示该记录未提交且可能发生更改。该标志可以是阻止其他事务访问记录的锁,也可以是指示其他事务应该谨慎地处理记录的一个警告。这个标志会被一个可重复的事务清除,这表示 saga 成功完成;或通过补偿事务清除,这表示 saga 发生了回滚。
管理语义锁只是问题的一半。你还需要根据具体情况决定一个 saga 应该如何处理已被锁定的记录。例如,考虑 cancelOrder() 这个系统命令。客户端可能会调用此操作来取消处于 approval_pending 状态的 order。
- 让 cancelOrder() 系统命令执行失败并告诉客户端稍后再试。优:易于实现,缺:使客户端更复杂,必须实现重试逻辑
- 让 cancelOrder() 处于阻塞状态,直到其他 saga 释放了语义锁。优:实质上重新创建了 ACID 事务提供的隔离。消除客户端重试的负担。缺:应用程序必须管理锁,还必须实现死锁检测算法,该算法执行 saga 的回滚以打破死锁并重新执行它。
对策:交换式更新
如果可以按任何顺序执行,则操作是可交换的(commutative)
悲观视图
重新排序 saga 的步骤,以最大限度地降低由于脏读而导致的业务风险。
对策:重读值
可防止丢失更新。使用此计数器的 saga 在更新之前重新读取记录,验证它是否未更改,然后更新记录。如果记录已更改,则 saga 将中止并可能重新启动。此对策是乐观脱机锁模式的一种。
对策:版本文件
版本文件对策之所以如此命名,是因为它记录了对数据执行的操作,以便可以对它们进行重新排序。这是将不可交换操作转换为可交换操作的一种方法。要了解此对策的工作原理,请考虑Create Order Saga与Cancel Order Saga同时执行的场景。除非Saga使用语义锁对策,否则cancel Order Saga可能会在Create Order Saga授权信用卡之前取消消费者信用卡的授权。
Accounting Service处理这些无序请求的一种方法是在操作到达时记录操作,然后以正确的顺序执行操作。在这种情况下,它将首先记录Cancel Authorization请求。然后,当Accounting Service收到后续的Authorize Card请求时,它会注意到它已经收到Cancel Authorization请求并跳过授权信用卡。
对策:业务风险评级
最终的对策是基于价值(业务风险)对策。这是一种基于业务风险选择并发机制的策略。使用此对策的应用程序使用每个请求的属性来决定使用Saga和分布式事务。它使用Saga执行低风险请求,可能会应用前几节中描述的对策。但它使用分布式事务来执行高风险请求(例如涉及大量资金)。此对策使应用程序能够动态地对业务风险、可用性和可伸缩性进行权衡。

浙公网安备 33010602011771号