概述
系列定位说明
本文是“分布式事务工程实践”系列的第四篇。在前三篇中,我们分别深入探讨了 XA 规范与 2PC 实现(第一篇)、Seata AT 模式源码深度解析(第二篇)以及 TCC 模式实现与异常恢复(第三篇)。从强一致性的 XA,到自动补偿的 AT,再到资源预留的 TCC,我们已经构建起对“短事务”(秒级内完成)的完整理解。本篇将跨越到分布式事务中最棘手的领域——长事务(Long-Lived Transaction),而 Saga 模式正是应对这一场景的唯一工程可行性方案。
理解 Saga 如何通过逆序补偿链在分钟级甚至小时级的长事务中实现最终一致性,以及**编排(Orchestration)与编制(Choreography)**两种协调模式在中心化控制与松耦合之间的深刻取舍,是架构师掌握分布式事务从“短事务”到“长事务”这一质变的核心跨越。
总结性引言
设想一个跨国汇款场景:一笔资金从美国的账户发起,中间经过换汇服务,最终计入中国某银行的账户。这个过程涉及多个独立的金融系统,可能需要 5-30 分钟才能完成最终确认。再设想一个机票-酒店-租车预订场景:用户可能先预订了一张机票,30 分钟后才决定添加酒店,再过 20 分钟才预订租车服务。这些场景的共同特征是:业务事务的持续时长远远超过了 XA 或 AT 等模式所能承受的秒级限制。数据库锁不可能被持有数分钟,TCC 的 Try 预留也同样不适用——你无法冻结一个航班座位或酒店房间长达 30 分钟而不释放。
Saga 模式是长事务的唯一解法。它由 Hector Garcia-Molina 在 1987 年提出,核心思想是把一个全局事务拆分为一系列有序的本地事务 T1, T2, ..., Tn,每个本地事务 Ti 都有对应的补偿事务 Ci。每个 Ti 执行成功后立即提交,释放数据库锁;若某个 Tk 失败,则逆序执行已提交事务的补偿逻辑 C(k-1) → ... → C2 → C1,最终让系统回到一致状态。当你使用 Seata Saga 的状态机 DSL 去定义“订票→订酒店→租车”三步 Saga,当你通过 Eventuate Tram 的 OrderCreated → PaymentCompleted → InventoryDeducted 事件链驱动微服务协作,当你利用 Camunda BPMN 流程引擎在人工审批节点等待经理确认——这背后的工程本质,都是 Saga 对“长时间跨度的分布式事务如何不持有锁而最终一致”的体系化回答。
本文将从 Saga 的补偿链原理出发,深入剖析 Choreography 与 Orchestration 两种协调模式,结合 Seata Saga、Eventuate Tram、Camunda BPMN 三个主流框架的源码级实现与配置,完整拆解 Saga 长事务的工程内核。
核心要点
- Saga 核心原理:将全局事务拆分为
T1, T2, ..., Tn本地事务序列,每个 Ti 对应一个补偿事务Ci;当 Tk 失败时,逆序执行C(k-1) → ... → C1,Ci 必须幂等且最终成功。 - Choreography(编制)模式:事件驱动、无中心协调者,服务通过消息队列发布/订阅领域事件实现正向操作与补偿,松耦合但追踪困难。
- Orchestration(编排)模式:中心化 Saga 协调器(状态机引擎)按状态机 DSL 依次调用各服务的 Ti,失败时逆序调用 Ci,具备全局事务状态追踪与持久化能力,易于监控。
- Seata Saga:基于 JSON/YAML 状态机 DSL,
StateMachineEngine解析并驱动执行,ServiceInvoker支持多种 RPC 调用,内置重试、超时与异步策略。 - Eventuate Tram:
@SagaOrchestrator注解 +@SagaCommandHandler/@SagaCompensatingHandler注解,通过 Kafka 实现事件驱动的 Saga 编制。 - Camunda Saga:基于 BPMN 2.0 流程引擎,
Service Task为正向操作,Compensation Boundary Event为补偿操作,支持可视化建模与人工审批节点。 - 补偿失败处理:自动重试(指数退避) + 人工介入(Admin 后台手动重试/跳过) + 向前恢复(Forward Recovery)。
- 与 TCC/AT 的对比:Saga 无 Try 预留阶段,隔离性弱于 TCC;Saga 的 Ci 是手写业务级补偿,不同于 AT 的
undo_log自动补偿;Saga 的锁持有时间为零,适合分钟~小时级的长事务。
文章组织架构图
flowchart LR
root(("Saga编排与长事务"))
subgraph A ["1. Saga核心原理"]
A1["本地事务序列T1-Tn"]
A2["逆序补偿链C1-Cn"]
A3["Ci三大设计原则"]
A4["隔离性缺失与语义锁"]
A5["长事务的业务特征"]
end
subgraph B ["2. Choreography编制模式"]
B1["事件驱动松耦合"]
B2["补偿事件流"]
B3["Eventuate Tram实现"]
B4["优缺点分析"]
end
subgraph C ["3. Orchestration编排模式"]
C1["状态机DSL驱动"]
C2["Seata Saga引擎"]
C3["ServiceInvoker"]
C4["重试与超时配置"]
C5["源码关键类解析"]
end
subgraph D ["4. Camunda BPMN Saga"]
D1["BPMN2.0流程建模"]
D2["CompensationEvent"]
D3["人工审批节点"]
D4["流程引擎逆序补偿机制"]
end
subgraph E ["5. 补偿失败处理策略"]
E1["自动重试机制"]
E2["人工介入后台"]
E3["向前恢复"]
E4["死信队列与告警"]
end
subgraph F ["6. 三种Saga框架对比"]
F1["Seata vs Eventuate vs Camunda"]
F2["选型决策树"]
end
subgraph G ["7. Saga与TCC/AT/XA的本质差异"]
G1["锁持有时间对比"]
G2["隔离性对比"]
G3["侵入性对比"]
G4["适用事务时长对比"]
G5["性能与并发对比"]
end
subgraph H ["8. 面试高频专题"]
H1["15+题含系统设计题"]
end
root --> A
root --> B
root --> C
root --> D
root --> E
root --> F
root --> G
root --> H
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
架构图说明
- 总览说明:全文 8 个模块从 Saga 的理论根基(核心原理与补偿链设计)出发,逐步深入到两种协调模式(Choreography 与 Orchestration)及其在三个主流框架中的工程实现,再到补偿失败的工业级处理策略,最后通过横向对比与高频面试题收尾,形成完整的知识闭环。
- 逐模块说明:
- 模块 1 建立 Saga 的理论根基:本地事务序列、逆序补偿链、Ci 的幂等设计原则,以及长事务的业务特征。
- 模块 2-3 是 Saga 的两种核心协调模式及其工业实现:Eventuate Tram 的事件驱动 Choreography vs Seata Saga 的状态机驱动的 Orchestration,包含源码级关键类剖析。
- 模块 4 是 BPMN 在 Saga 中的特殊应用:引入 Camunda 引擎处理包含人工审批的复杂长事务,并剖析流程引擎逆序补偿的内部机制。
- 模块 5 聚焦补偿失败这一生产环境核心问题,给出重试、人工介入与向前恢复的完整方案,并引入死信队列与告警设计。
- 模块 6-7 是横向对比与选型决策,将 Saga 与 Seata AT、TCC、XA 进行多维度比较,包括性能与并发方面的量化分析。
- 模块 8 是面向面试的实战强化,包含 15+ 题目,每题遵循“一句话回答+详细解释+多角度追问+加分回答”四段结构。
- 关键结论:Saga 通过将全局事务拆分为多个立即提交的本地事务,并通过逆序补偿链实现最终一致性,是长事务(5 分钟以上)的唯一可行方案。Orchestration 编排模式提供中心化状态追踪与易于监控的能力,而 Choreography 编制模式则提供极致的服务松耦合。Saga 的隔离性弱于 TCC 和 AT,但业务侵入性低于 TCC,是柔性分布式事务中时间跨度最长、对锁依赖最小的终极方案。
1. Saga 核心原理:本地事务序列与逆序补偿链
Saga 将一次完整的业务操作(如跨行汇款、旅行预订)建模为一个由多个步骤组成的长事务。每个步骤都是一个本地事务 Ti,它会修改本地数据库,并发布某种形式的“下一步”指令或事件。最关键的是,每个 Ti 执行完成后立即提交本地事务,释放数据库锁。若后续步骤失败,Saga 则从失败点开始,逆序依次执行已成功步骤的补偿事务 Ci,即 T1, T2, ..., Tk(失败) → C(k-1), ..., C2, C1。
1.1 长事务的业务特征与 ACID 妥协
传统数据库事务的 ACID 特性中,隔离性(Isolation) 和 持久性(Durability) 依赖于锁和日志。当业务操作跨越多个分钟级的远程调用时,持有数据库锁将导致灾难性的并发性能。Saga 的做法是:
- 放弃隔离性:Ti 立即提交,其后效应对其它事务可见,产生脏读。
- 持久性保留但可撤销:Ti 的提交是永久的,但通过执行 Ci 可以从业务上撤销其影响,这是一种语义回滚。
- 原子性由补偿链保证:通过逆序补偿,最终使系统恢复到一致性状态,但这种原子性是最终的,且允许中间不一致窗口。
1.2 逆序补偿的必然性
Saga 的补偿链必须严格逆序执行,这是由其业务语义决定的。假设正向步骤为:T1: 创建订单 → T2: 扣减库存 → T3: 扣款。若 T3 扣款失败,此时 T1 订单已创建,T2 库存已扣减。要回滚整个业务,必须先补偿 T2(C2: 恢复库存),再补偿 T1(C1: 取消订单)。如果进行正序补偿 C1 → C2,那么在 C1 取消订单后,C2 恢复库存可能因订单已不存在而产生数据不一致(比如库存恢复时依赖订单中的某些信息)。逆序补偿保证了数据依赖关系的正确性:后执行的步骤依赖前一步骤的结果,因此补偿时须逆向消除依赖。
1.3 Ci 的三大设计原则
- 幂等性:Ci 可能因网络重试等原因被多次调用,必须保证其执行一次和多次的效果相同。通常通过业务唯一标识(如
bizId + actionType)和状态机来实现。 - 最终成功:Ci 在遇到临时错误(如网络超时、服务不可用)时,应支持重试,且重试逻辑必须确保最终能够成功。若重试耗尽仍失败,则需人工介入。
- 上下文传递:Ci 在执行时需要 Ti 产生的业务数据来执行补偿。例如,补偿扣款需要原始的付款流水号,补偿创建订单需要订单 ID。这些数据必须在 Ti 执行时持久化到 Saga 的上下文或本地业务表中。
1.4 Saga 的隔离性缺失与语义锁
Saga 的致命弱点在于隔离性缺失。因为 Ti 会立即提交本地事务,在整个 Saga 未完成前,其他事务可以读到并操作部分完成的数据。例如,在“订单创建”和“付款扣减”之间,用户能看到一个“待付款”订单,甚至可能并发修改该订单。
防御手段是使用语义锁(Semantic Lock):在业务表中引入状态字段,并通过应用层逻辑来控制并发。例如订单表设计:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL,
status VARCHAR(20) NOT NULL DEFAULT 'CREATED', -- CREATED, PAID, CANCELLED
gmt_create DATETIME,
gmt_modified DATETIME
);
在 T1 创建订单时,状态为 CREATED。T2 扣款成功后将状态更新为 PAID。补偿 C1 取消订单时,将状态从 CREATED 更新为 CANCELLED。当其他事务读取到 CREATED 状态时,可以明确知道此订单尚未完成,从而决定自己的操作策略(如库存服务可能只对 PAID 订单进行发货)。这种应用层的状态机是 Saga 模式防脏读的核心手段。
1.5 Saga 架构全景:组件与交互
一个完整的 Saga 系统(无论是编制还是编排模式)在宏观上由几个关键组件构成。理解这些组件的职责和交互关系,是掌握 Saga 工程落地的基础。
1.5.1 整体架构图
flowchart TB
Client[业务发起方<br/>Client]
subgraph Saga_Control_Plane [Saga 控制面]
Coordinator[Saga 协调器<br/>- 编排: 状态机引擎<br/>- 编制: 事件编排器]
StateStore[(状态持久化<br/>state_machine_inst<br/>state_inst / 事件日志)]
end
subgraph Services [参与服务]
SvcA[服务A<br/>Ti / Ci]
SvcB[服务B<br/>Ti / Ci]
SvcC[服务C<br/>Ti / Ci]
end
subgraph Infra [基础设施]
MQ[消息队列<br/>Kafka / RabbitMQ<br/>仅编制模式]
CompLog[(补偿日志<br/>saga_compensation_log)]
end
Admin[Admin 后台<br/>人工介入]
Monitor[监控告警<br/>Prometheus / Grafana]
Client -- 启动Saga --> Coordinator
Coordinator -- 调用Ti/Ci --> Services
Coordinator -- 读写状态 --> StateStore
Coordinator -- 记录补偿失败 --> CompLog
Coordinator -. 编制模式发布/订阅事件 .-> MQ
MQ -. 事件驱动 .-> Services
Admin -- 重试/跳过补偿 --> CompLog
Admin -- 更新状态 --> StateStore
Monitor -- 采集指标 --> StateStore
Monitor -- 采集失败数 --> CompLog
1.5.2 架构分层说明
- 整体结构:Saga 系统分为控制面、参与服务和基础设施三个部分。控制面负责流程编排与状态追踪;参与服务是业务逻辑的载体;基础设施提供持久化、消息通信、人工干预和监控能力。
- Saga 协调器(Coordinator):
- 在**编排(Orchestration)**模式下,协调器是一个中心化的状态机引擎(如 Seata
StateMachineEngine)。它持有全局事务的完整状态,按序调用各服务并处理补偿。 - 在**编制(Choreography)**模式下,协调器通常演变为一个事件编排器(如 Eventuate Tram 的
SagaOrchestrator),它负责发送命令并监听回复事件,但仍集中定义了 Saga 流程,只是执行过程依赖于消息队列。
- 在**编排(Orchestration)**模式下,协调器是一个中心化的状态机引擎(如 Seata
- 状态持久化(StateStore):协调器将 Saga 的执行历史和当前状态写入数据库(如 Seata 的
state_machine_inst和state_inst表)。这是 Saga 实现断点续跑和故障恢复的关键。即使协调器宕机,重启后也能从持久化状态中恢复并继续执行未完成的步骤或补偿。 - 参与服务(Services):每个服务都实现了正向操作 Ti 和补偿操作 Ci,通过 RPC 或消息队列被协调器调用。服务本身需要保证 Ti 和 Ci 的幂等性,并遵循上下文传递约定。
- 消息队列(MQ):在编制模式中,消息队列是各服务间异步通信的枢纽。正向事件和补偿事件通过 Topic 进行传递,实现彻底的松耦合。编排模式通常直接使用 RPC 调用,不强制依赖 MQ。
- 补偿日志(CompensationLog):当自动重试补偿失败后,需要将失败信息写入专门的
saga_compensation_log表,作为人工介入的数据源。该表独立于状态机运行日志,聚焦于补偿失败场景。 - Admin 后台与监控(Admin & Monitor):提供运维人员手动处理失败补偿的界面(重试、跳过、标记解决),并与 Prometheus 等系统集成,监控 Saga 成功率、执行时长、失败数量等指标,触发告警。
1.5.3 一次完整的 Saga 调用路径
- 启动:客户端发起业务请求,协调器根据请求参数创建 Saga 实例,并持久化初始状态。
- 正向执行:协调器按预定义步骤依次调用各服务的 Ti。每成功一步,记录状态并持久化;若某一步失败,立即终止正向流程,转入补偿流程。
- 补偿执行:协调器从持久化的成功步骤列表中,逆序取出补偿任务,并调用对应的 Ci。补偿调用同样会重试,成功则继续下一个补偿,失败则根据策略记录补偿日志并告警。
- 终态:所有正向步骤成功,Saga 进入成功终态;所有补偿步骤完成,Saga 进入失败(但已回滚)终态;若补偿重试耗尽仍未成功,Saga 停留在“中间不一致”状态,等待人工处理。
这种组件化的架构设计,使得 Saga 既能支撑极其松散的事件驱动协同,也能实现中心化强管控的长事务,是分布式事务从“短事务”迈向“长事务”的工程基石。
1.6 正向执行与逆序补偿链时序图
下图展示了一个三步 Saga(订单创建→库存扣减→付款)的完整流程:T3 付款失败后,协调器逆序触发 C2 恢复库存,然后 C1 取消订单,最终系统回到全局回滚完成状态。
sequenceDiagram
participant SagaCoordinator as Saga协调器
participant ServiceA as 订单服务(T1/C1)
participant ServiceB as 库存服务(T2/C2)
participant ServiceC as 付款服务(T3)
SagaCoordinator->>ServiceA: T1 创建订单
ServiceA-->>SagaCoordinator: 成功(订单ID=101)
SagaCoordinator->>ServiceB: T2 扣减库存
ServiceB-->>SagaCoordinator: 成功
SagaCoordinator->>ServiceC: T3 扣款
ServiceC--xSagaCoordinator: 失败(余额不足)
Note over SagaCoordinator: T3失败,开始逆序补偿
SagaCoordinator->>ServiceB: C2 恢复库存
ServiceB-->>SagaCoordinator: 补偿成功
SagaCoordinator->>ServiceA: C1 取消订单(101)
ServiceA-->>SagaCoordinator: 补偿成功
Note over SagaCoordinator: 全局事务已回滚完成
图说明:
- 参与者:Saga 协调器(Orchestration 模式)或事件路由(Choreography 模式)作为流程大脑,调用订单、库存、付款三个服务。
- 正向流程:协调器按序调用 T1、T2,均成功;直到 T3 返回失败,触发补偿链。
- 补偿链:协调器严格逆序调用 C2 恢复库存,再调用 C1 取消订单,最终达到数据全局一致。
- 关键点:每个 Ti 在执行完成时即提交本地事务并释放锁;补偿 Ci 也是独立的本地事务,与 Ti 完全解耦。
2. Choreography 编制模式:事件驱动松耦合 + Eventuate Tram
Choreography(编制)模式将 Saga 的控制逻辑分布到各个服务中,没有中心协调器。每个服务监听上游的领域事件,执行自己的本地事务 Ti,然后将成功或失败的领域事件发布给下游。如果某一步失败,它会发布一个补偿事件,上游服务监听到后执行对应的 Ci。
2.1 事件驱动 Saga 的原理
在一个典型的编制模式中,事件如同一条链条在服务间流转:
- Order 服务 创建订单并发布
OrderCreated事件。 - Payment 服务 监听到
OrderCreated,执行扣款。若成功,发布PaymentCompleted事件;若失败,发布PaymentFailed事件。 - Inventory 服务 监听到
PaymentCompleted,扣减库存。若成功,发布InventoryDeducted,Saga 成功结束;若失败,发布InventoryDeductionFailed事件。 - Payment 服务 监听到
InventoryDeductionFailed,执行补偿(退款)并发布PaymentRefunded。 - Order 服务 监听到
PaymentRefunded,执行补偿(取消订单)并发布OrderCancelled。
整个过程中,服务之间完全通过事件进行交互,彼此不知道对方的存在,实现了极致的松耦合。
2.2 Eventuate Tram 的实现与源码剖析
Eventuate Tram 是一个事件驱动的 Saga 框架,它提供了 @SagaOrchestrator 等注解来简化编制模式的开发。尽管其注解名为 Orchestrator,但 Eventuate Tram 的核心模型是基于事件和命令的编制。它定义了一个 Saga 流程,其中包含一系列步骤,每个步骤发送命令到目标服务,并根据命令的回复(成功或失败事件)决定下一步动作。
示例:Eventuate Tram 的 Saga 定义
@SagaOrchestrator
public class CreateOrderSaga {
private final SagaCommandProducer sagaCommandProducer;
// 第一步:命令“创建订单”
@SagaCommandHandler( command = "createOrder" )
public SagaCommandResponse handleCreateOrder(CreateOrderCommand cmd) {
// 发送 createOrder 命令到 Order 服务
sagaCommandProducer.sendCommand("orderService", cmd);
// 期待回复事件:OrderCreatedEvent
return SagaCommandResponse.withExpectingReplies();
}
// 第二步:命令“扣款”
@SagaCommandHandler( command = "processPayment" )
public SagaCommandResponse handleProcessPayment(ProcessPaymentCommand cmd) {
sagaCommandProducer.sendCommand("paymentService", cmd);
return SagaCommandResponse.withExpectingReplies();
}
// 补偿:取消订单
@SagaCompensatingHandler(method = "cancelOrder")
public void handleCancelOrder(CancelOrderCommand cmd) {
sagaCommandProducer.sendCommand("orderService", cmd);
}
// 补偿:退款
@SagaCompensatingHandler(method = "refundPayment")
public void handleRefundPayment(RefundPaymentCommand cmd) {
sagaCommandProducer.sendCommand("paymentService", cmd);
}
}
源码解读:
@SagaOrchestrator声明这是一个 Saga 的流程定义,框架会管理其生命周期。@SagaCommandHandler标记的方法代表 Saga 的一个正向步骤(发送一个命令)。expectingReplies表明该步骤会等待目标服务的异步回复。@SagaCompensatingHandler(method = "cancelOrder")定义了正向命令createOrder对应的补偿操作cancelOrder。当 Saga 后续步骤失败时,框架会自动逆序调用所有已成功步骤的CompensatingHandler。SagaCommandProducer负责将命令消息发送到消息队列(如 Kafka),目标服务通过订阅命令主题来消费命令并执行本地事务,完成后发布回复事件。框架根据回复事件(成功/失败)驱动 Saga 状态转换。
2.3 Choreography 的优缺点与适用场景
- 优点:极致的松耦合,服务间无直接依赖,易于扩展新步骤(只需添加新的事件监听器)。
- 缺点:补偿链追踪极其困难。整个 Saga 的状态分散在事件流和各个服务中,没有单一的监控点。开发者必须梳理事件链才能理解一次 Saga 调用的完整路径,调试和排障成本高。此外,服务间通过事件隐式编排,可能出现循环依赖或事件风暴。
2.4 编制模式的事件链与补偿事件流图
flowchart LR
subgraph 正向事件流
A[OrderCreated] --> B[PaymentCompleted]
B --> C[InventoryDeducted]
end
subgraph 补偿事件流
C -- 失败 --> D[InventoryDeductionFailed]
D --> E[PaymentRefunded]
E --> F[OrderCancelled]
end
style A fill:#e1f5fe
style B fill:#e1f5fe
style C fill:#e1f5fe
style D fill:#ffcdd2
style E fill:#ffcdd2
style F fill:#ffcdd2
图说明:
- 事件节点:蓝色代表正向事件流,红色代表补偿事件流。
- 正向链路:
OrderCreated(订单服务发布)→PaymentCompleted(支付服务发布)→InventoryDeducted(库存服务发布),形成链式调用。 - 补偿触发:当
InventoryDeducted因库存不足失败时,库存服务发布InventoryDeductionFailed。支付服务监听到后执行退款并发布PaymentRefunded,订单服务监听到后取消订单并发布OrderCancelled。整个补偿链通过事件逆序传递。 - 无中心性体现:整个流程中没有一个中心的控制器,每一步都是由事件消费者自发完成并发布新事件。
3. Orchestration 编排模式:状态机 DSL + Seata Saga 引擎
Orchestration(编排)模式引入一个中心化的 Saga 协调器(通常实现为一个状态机引擎),它负责维护全局事务的状态,按预定义的步骤依次调用各服务的 Ti,并记录成功与失败。当某一步失败时,协调器会自动逆序调用已成功步骤的 Ci,直至补偿完成。
Seata 的 Saga 模式是 Orchestration 的典型实现。它提供了一个状态机 DSL,允许开发者以 JSON 或 YAML 的形式定义 Saga 流程,由 Seata 的 StateMachineEngine 负责解析与驱动。
3.1 Seata Saga 状态机 DSL 剖析
一个 Seata Saga 状态机定义包含以下几个核心部分:
- Name:状态机名称,全局唯一。
- StartState:初始状态。
- States:定义流程中的所有状态(步骤)。
- ServiceTask:执行一个服务调用(即 Ti)。
- CompensationTask:绑定的补偿任务(即 Ci),但通常直接在正向状态中通过
CompensateState属性绑定。 - Choice:条件分支,根据表达式路由到不同目标状态。
- Succeed / Fail:全局终态,标记事务成功或失败。
- Transitions:状态间的转换规则,定义了从当前状态在何种条件(成功/失败/异常)下跳转到哪个状态。
Seata Saga 完整状态机 JSON 定义(订票→订酒店→租车)
{
"Name": "TravelBookingSaga",
"Comment": "旅行预订三步Saga:订票→订酒店→租车",
"StartState": "BookFlight",
"Version": "1.0",
"States": {
"BookFlight": {
"Type": "ServiceTask",
"ServiceName": "flightService",
"ServiceMethod": "bookFlight",
"CompensateState": "CancelFlight",
"Next": "BookHotel",
"Input": ["$.[tripId]", "$.[userId]"],
"Output": { "flightBookingId": "$.bookingId" },
"Status": { "#root == true ? 'SU' : 'FA'" }
},
"BookHotel": {
"Type": "ServiceTask",
"ServiceName": "hotelService",
"ServiceMethod": "bookHotel",
"CompensateState": "CancelHotel",
"Next": "RentCar",
"Input": ["$.[tripId]", "$.[userId]"],
"Output": { "hotelBookingId": "$.bookingId" },
"Status": { "#root == true ? 'SU' : 'FA'" }
},
"RentCar": {
"Type": "ServiceTask",
"ServiceName": "carService",
"ServiceMethod": "rentCar",
"CompensateState": "CancelCar",
"Next": "Succeed",
"Input": ["$.[tripId]", "$.[userId]"],
"Output": { "carRentalId": "$.rentalId" },
"Status": { "#root == true ? 'SU' : 'FA' },
"Retry": [
{
"Exceptions": ["io.seata.saga.exception.TransactionException"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
]
},
"CancelCar": {
"Type": "ServiceTask",
"ServiceName": "carService",
"ServiceMethod": "cancelCar",
"Next": "CancelHotel",
"Input": ["$.[carRentalId]"],
"Status": { "#root == true ? 'SU' : 'FA'" }
},
"CancelHotel": {
"Type": "ServiceTask",
"ServiceName": "hotelService",
"ServiceMethod": "cancelHotel",
"Next": "CancelFlight",
"Input": ["$.[hotelBookingId]"],
"Status": { "#root == true ? 'SU' : 'FA'" }
},
"CancelFlight": {
"Type": "ServiceTask",
"ServiceName": "flightService",
"ServiceMethod": "cancelFlight",
"Next": "Fail",
"Input": ["$.[flightBookingId]"],
"Status": { "#root == true ? 'SU' : 'FA'" }
},
"Succeed": {
"Type": "Succeed"
},
"Fail": {
"Type": "Fail",
"ErrorCode": "TRAVEL_BOOKING_FAILED",
"Message": "旅行预订Saga失败"
}
}
}
配置解读:
- 正向链路:
BookFlight→BookHotel→RentCar→Succeed。每个ServiceTask通过CompensateState属性绑定了对应的补偿任务,如BookFlight的补偿是CancelFlight。 - 补偿链路:
RentCar失败(或异常)时,引擎会自动触发补偿链:CancelCar→CancelHotel→CancelFlight→Fail。这个逆序执行逻辑由引擎根据CompensateState和当前正向执行栈自动构建。 - 重试策略:
RentCar状态定义了Retry策略:当发生TransactionException时,重试间隔 5 秒,最大 3 次,退避指数 2.0(即 5s, 10s, 20s)。 - 上下文传递:通过
Input表达式(SpEL)从状态机上下文注入业务参数,通过Output将服务返回结果存入上下文,供后续步骤或补偿步骤使用。
3.2 StateMachineEngine 核心流程与源码剖析
Seata 的 ProcessCtrlStateMachineEngine 是状态机的核心执行引擎,其工作流程如下:
- 加载状态机定义:
StateMachineConfig负责从配置源(如 Nacos、本地文件)加载状态机 JSON/YAML 定义,解析为StateMachine对象,并缓存。 - 创建状态机实例:每次 Saga 启动时,引擎根据状态机名称创建一个新的实例
StateMachineInstance,并持久化到state_machine_inst表:CREATE TABLE state_machine_inst ( id VARCHAR(128) PRIMARY KEY, machine_id VARCHAR(128) NOT NULL, tenant_id VARCHAR(128), parent_id VARCHAR(128), gmt_started DATETIME, gmt_updated DATETIME, gmt_end DATETIME, status VARCHAR(16), -- RU(运行中), SU(成功), FA(失败), UN(未知) compensation_status VARCHAR(16), -- 补偿状态 running TINYINT, start_params LONGTEXT, exception LONGTEXT ); - 驱动状态转换:
StateMachineEngine的核心是事件循环。它根据当前状态,通过Transition找到下一个状态并执行。ServiceTaskState的执行依赖ServiceInvoker。 - 记录执行历史:每执行完一个状态,引擎都会在
state_inst表中记录:CREATE TABLE state_inst ( id VARCHAR(128) PRIMARY KEY, machine_inst_id VARCHAR(128) NOT NULL, name VARCHAR(128), type VARCHAR(16), -- ServiceTask, Choice, Succeed, Fail status VARCHAR(16), compensation_status VARCHAR(16), input_params LONGTEXT, output_params LONGTEXT, exception LONGTEXT, gmt_started DATETIME, gmt_updated DATETIME, gmt_end DATETIME ); - 触发补偿:若状态执行失败,引擎会获取已执行成功的正向状态历史(从
state_inst表中按执行顺序查询),逆序构建补偿状态链(通过CompensateState属性),并依次执行补偿状态。 - 重试机制:
Retry配置由RetryInterceptor处理,当ServiceTask执行抛出匹配的异常时,根据IntervalSeconds、BackoffRate和MaxAttempts进行重试。
3.3 ServiceInvoker 与服务调用抽象
ServiceInvoker 是 Seata Saga 调用远程服务的抽象层,它支持多种 RPC 协议:
- Spring Cloud (HTTP):通过
RestTemplate调用。 - Dubbo:通过
DubboProxy调用。 - gRPC:通过 gRPC Stub 调用。
- Local Bean:直接调用本地 Spring Bean 的方法。
开发者只需在状态机 DSL 中指定 ServiceName(如 Dubbo 的 reference ID 或 Spring Bean 名称)和 ServiceMethod,引擎会自动选择合适的 ServiceInvoker 实现。
3.4 状态机驱动序列图
sequenceDiagram
participant Client as 业务发起方
participant Engine as StateMachineEngine
participant Store as StateMachineInstanceStore
participant Invoker as ServiceInvoker
participant SvcA as 订单服务(T1/C1)
participant SvcB as 库存服务(T2/C2)
Client->>Engine: 启动状态机(TravelBooking)
Engine->>Store: 创建状态机实例(status=RU)
Store-->>Engine: 实例ID=1001
Engine->>Invoker: 执行StateA (订单T1)
Invoker->>SvcA: 调用createOrder
SvcA-->>Invoker: 成功(orderId=101)
Invoker-->>Engine: 返回成功
Engine->>Store: 记录StateA实例(status=SU)
Engine->>Invoker: 执行StateB (库存T2)
Invoker->>SvcB: 调用deductInventory
SvcB-->>Invoker: 失败(库存不足)
Invoker-->>Engine: 返回失败
Note over Engine: T2失败,触发逆序补偿
Engine->>Invoker: 执行补偿State C1 (CancelOrder)
Invoker->>SvcA: 调用cancelOrder(101)
SvcA-->>Invoker: 补偿成功
Invoker-->>Engine: 返回成功
Engine->>Store: 记录补偿C1实例(status=SU)
Engine->>Store: 更新状态机实例状态(status=FA)
Engine-->>Client: 全局事务失败(已完全补偿)
图说明:
- 协调器:
StateMachineEngine作为中心协调器,全程控制调用顺序。 - 正向执行:引擎先执行 T1 并记录成功;执行 T2 时得到失败响应,立即停止正向执行。
- 逆序补偿:引擎根据已执行的步骤列表
[StateA(SU)],触发其补偿任务CancelOrder,执行成功后更新全局状态为失败。 - 持久化:所有步骤状态均通过
StateMachineInstanceStore持久化到数据库,保证在引擎宕机后能恢复执行。
3.5 Seata Saga 状态机状态转换图
下面展示了上述 JSON 定义的状态转换逻辑(DSL 结构视图)。
stateDiagram-v2
[*] --> BookFlight : 启动
BookFlight --> BookHotel : T1成功
BookFlight --> Fail : T1失败
BookHotel --> RentCar : T2成功
BookHotel --> CancelFlight : T2失败
RentCar --> Succeed : T3成功
RentCar --> CancelCar : T3失败
CancelCar --> CancelHotel : C3完成
CancelHotel --> CancelFlight : C2完成
CancelFlight --> Fail : C1完成
Succeed --> [*]
Fail --> [*]
图说明:
- 状态节点:
BookFlight、BookHotel、RentCar是正向ServiceTask;CancelCar、CancelHotel、CancelFlight是补偿ServiceTask。 - 转换路径:实线箭头代表成功转换,虚线箭头代表失败转换。任何正向步骤失败都会进入其对应的补偿状态,并沿着补偿链最终到达
Fail终态。 - 关键设计:补偿链的路径在状态机定义中就被固定下来(通过
CompensateState),引擎在执行时无需动态推导,健壮性极高。
4. Camunda BPMN Saga 编排:流程引擎 + 可视化 + 人工审批
对于包含人工审批节点、复杂条件分支或需要长期等待(如数天)的长事务,BPMN 流程引擎(如 Camunda)提供了一种更加高级且可视化的 Saga 实现方案。Camunda 基于 BPMN 2.0 规范,允许开发者通过流程图定义 Saga 步骤,并利用其 Compensation Boundary Event 机制实现补偿逻辑。
4.1 BPMN 2.0 在 Saga 中的应用
在 BPMN 流程图中:
- Service Task:代表一个正向操作
Ti,通常绑定一个 Java 委托类或外部 REST/gRPC 调用。 - Compensation Boundary Event:附加在 Service Task 边界上的事件,定义了当流程需要补偿时执行的
Ci。 - Exclusive Gateway:条件分支,实现类似 Seata Saga 的
Choice功能。
当流程运行到某个 Service Task 失败或流程被中断时,Camunda 引擎会自动回溯已完成的 Service Task,并逆序触发它们所绑定的 Compensation Boundary Event,执行补偿逻辑。Camunda 通过 ActivityStack 记录已完成的补偿范围(Compensation Scope),确保补偿的逆序性。
Camunda BPMN Saga 流程图 XML 示例
<bpmn:process id="TravelBookingSaga" name="旅行预订BPMN Saga" isExecutable="true">
<bpmn:startEvent id="start" />
<!-- T1: 订票 -->
<bpmn:serviceTask id="bookFlight" name="预订机票"
camunda:delegateExpression="${bookFlightDelegate}">
<!-- 绑定补偿事件 -->
<bpmn:boundaryEvent id="compensateBookFlight" name="取消预订机票"
attachedToRef="bookFlight" cancelActivity="false">
<bpmn:compensateEventDefinition id="compensateEventFlight" />
</bpmn:boundaryEvent>
</bpmn:serviceTask>
<!-- T2: 订酒店 -->
<bpmn:serviceTask id="bookHotel" name="预订酒店"
camunda:delegateExpression="${bookHotelDelegate}">
<bpmn:boundaryEvent id="compensateBookHotel" name="取消预订酒店"
attachedToRef="bookHotel" cancelActivity="false">
<bpmn:compensateEventDefinition id="compensateEventHotel" />
</bpmn:boundaryEvent>
</bpmn:serviceTask>
<!-- 人工审批节点 -->
<bpmn:userTask id="managerApproval" name="经理审批"
camunda:candidateGroups="managers">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="approved" label="审批结果" type="boolean" />
</camunda:formData>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- 正向流程流转 -->
<bpmn:sequenceFlow id="flow1" sourceRef="start" targetRef="bookFlight" />
<bpmn:sequenceFlow id="flow2" sourceRef="bookFlight" targetRef="bookHotel" />
<bpmn:sequenceFlow id="flow3" sourceRef="bookHotel" targetRef="managerApproval" />
<!-- 补偿触发: 使用补偿中间事件 -->
<bpmn:intermediateThrowEvent id="compensateTrigger" name="触发补偿">
<bpmn:compensateEventDefinition activityRef="bookHotel" />
<bpmn:compensateEventDefinition activityRef="bookFlight" />
</bpmn:intermediateThrowEvent>
<bpmn:endEvent id="end" />
</bpmn:process>
配置解读:
serviceTask通过camunda:delegateExpression绑定到 Spring 容器中的 Java 类(实现JavaDelegate接口),该类完成实际的业务调用。<bpmn:boundaryEvent>标签定义了该任务的补偿边界。当引擎需要补偿时,会执行compensateEventDefinition指定的逻辑。managerApproval是一个userTask,流程执行到此处会暂停,等待managers组的用户通过 Camunda Tasklist 完成审批。审批通过则继续后续步骤,审批不通过则可触发补偿。- 补偿触发顺序:引擎默认按照逆序触发各个已完成的 Service Task 的补偿边界事件,即先取消酒店,再取消机票。这与 Saga 的逆序补偿链完美契合。
4.2 Camunda 补偿机制源码级解析
Camunda 的补偿是基于 Compensation Scope 和 ActivityStack 实现的:
- 当一个具有
Compensation Boundary Event的 Activity 执行完成时,引擎会将其补偿信息压入ActivityStack。 - 当补偿事件触发时(例如
compensateTrigger),引擎从栈顶弹出补偿信息,执行对应的补偿 Activity,实现严格的逆序调用。 - 补偿 Activity 本身也可以有
Compensation Boundary Event,形成嵌套补偿,但 Saga 场景中通常不需要。
4.3 适用场景
Camunda BPMN 特别适合那些包含人工干预、复杂分支、长时间等待的长事务场景,例如:
- 供应链履约:下单→审核→发货→签收,中间可能因缺货需要人工干预。
- 保险理赔:报案→查勘→定损→审批→赔付,每步都可能有人工审核节点。
- 订单审批流:大额订单需经过部门经理、总监、财务三级审批。
5. 补偿失败的处理策略:重试、人工介入与向前恢复
在 Saga 的实现中,补偿 Ci 失败的处理是工程上的核心难点。因为此时正向 Ti 已提交,系统处于“中间不一致”状态。Saga 提供了三种层次的处理策略:
5.1 自动重试机制
补偿操作通常是可重试的临时性故障(如网络超时、目标服务临时不可用)。Saga 协调器应对 Ci 配置自动重试策略,例如 Seata Saga 中的 Retry 配置:
"Retry": [
{
"Exceptions": ["java.net.SocketTimeoutException"],
"IntervalSeconds": 2,
"MaxAttempts": 10,
"BackoffRate": 2.0
}
]
此配置表示:当 SocketTimeoutException 发生时,重试间隔 2 秒,最多 10 次,退避系数 2.0(间隔序列:2s, 4s, 8s, ..., 最大不超过 600s)。大多数瞬时故障可在重试数次后恢复。Seata Saga 的 RetryInterceptor 在 ServiceTask 执行时透明包装了此逻辑。
5.2 人工介入
若自动重试耗尽后补偿仍失败,必须由系统将失败信息持久化,并触发告警通知人工介入。工程实践中,需设计一张 saga_compensation_log 表:
CREATE TABLE saga_compensation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
saga_id VARCHAR(64) NOT NULL COMMENT 'Saga全局事务ID',
step_name VARCHAR(128) NOT NULL COMMENT '失败的补偿步骤名',
biz_key VARCHAR(256) COMMENT '业务主键',
error_message TEXT COMMENT '失败原因',
retry_count INT DEFAULT 0 COMMENT '已重试次数',
max_retry INT DEFAULT 10 COMMENT '最大重试次数',
status VARCHAR(16) NOT NULL COMMENT '状态:PENDING,RETRYING,FAILED,RESOLVED',
gmt_create DATETIME,
gmt_modified DATETIME,
UNIQUE KEY uk_saga_step (saga_id, step_name)
);
通过一个后台 Admin 系统,运维人员可以查看失败记录,手动执行“重试”、“跳过”(业务允许时)或“标记已解决”操作。这是保证系统最终一致性的最后防线。监控层面,可通过 Prometheus 监控 status='FAILED' 的记录数量,并配置 AlertManager 告警规则。
5.3 向前恢复(Forward Recovery)
当补偿代价太高或不可能时(例如,订单已发货,物理上无法“取消发货”),可以采用向前恢复策略。即不执行 Ci,而是继续执行一系列新的正向步骤,最终达到一个业务上可接受的终态。
例如,一个订单因地址错误发货失败,Ci 是退款并取消订单。但向前恢复可能是:通知用户更新地址 → 重新发货。这在业务上是更优的解决方案。Saga 协调器可在 Ci 失败后,根据业务规则决定是继续重试补偿还是触发一条向前恢复的子流程。
5.4 死信队列与告警
在 Eventuate Tram 等消息驱动框架中,补偿命令发送失败超过最大重试次数后,消息会进入死信队列(Dead Letter Queue)。需要专门的消费者处理死信,将其记录到人工介入表,并发送 PagerDuty/钉钉告警。
5.5 补偿失败处理决策树
flowchart TD
Start[补偿Ci执行] --> Result{执行结果?}
Result -- 成功 --> End[补偿完成]
Result -- 失败 --> Retry[自动重试<br>指数退避]
Retry --> RetryResult{重试是否成功?}
RetryResult -- 成功 --> End
RetryResult -- 耗尽 --> ManualDecision{是否可向前恢复?}
ManualDecision -- 是 --> Forward[执行向前恢复流程]
ManualDecision -- 否 --> Human[持久化失败日志<br>人工介入<br>Admin后台重试/跳过]
Human --> End
Forward --> End
图说明:
- 三个分支:补偿失败后,系统首先进入自动重试路径(路径1)。重试耗尽后,面临两种选择:若业务可向前恢复则走路径3,否则进入人工介入路径2。
- 状态持久化:进入人工介入路径时,必须将当前 Saga 上下文、失败步骤和错误详情完整写入
saga_compensation_log,确保信息不丢失。 - 最终一致性:无论哪条路径,最终目标都是让系统脱离中间不一致状态,达到业务定义的终态。
6. 三种 Saga 框架对比与选型决策
| 维度 | Seata Saga | Eventuate Tram | Camunda BPMN |
|---|---|---|---|
| 协调模式 | Orchestration(编排) | Choreography(编制) | Orchestration(编排) |
| 状态定义 | JSON/YAML 状态机 DSL | Java 注解 (@SagaOrchestrator) | BPMN 2.0 XML 流程图 |
| 状态持久化 | 内置 state_machine_inst/state_inst 表,自动记录 | 需自行实现或集成事件日志 | 引擎内置 ACT_ 系列表,完整记录 |
| 通信方式 | 通过 ServiceInvoker 同步/异步调用 (Dubbo, HTTP, gRPC) | 异步消息驱动 (Kafka) | 同步调用 JavaDelegate 或 REST,支持异步 |
| 补偿定义 | 状态机中的 CompensateState | @SagaCompensatingHandler 注解 | BPMN Compensation Boundary Event |
| 人工审批 | 不支持原生节点,需自定义 | 不支持 | 原生 UserTask 支持,内置任务列表 |
| 可视化监控 | 依赖 Seata Dashboard 或自研 | 无中心可视化,需聚合事件流 | Camunda Cockpit 提供流程实例图、性能指标 |
| 重试机制 | 内置,可配置指数退避 | 依赖消息队列重试 | 可通过 async 任务 + Job Executor 重试 |
| 社区与生态 | Seata 生态,阿里背书,Java 微服务首选 | Eventuate 生态,多语言支持 | 成熟 BPM 引擎,国际标准,生态丰富 |
| 典型场景 | 微服务间事务、异步业务流程 | 事件驱动架构、多语言环境 | 含人工审批、复杂分支的长业务流程 |
选型决策树
- 你的系统中是否已大规模使用 Seata 治理分布式事务?
- 是 → Seata Saga 是最佳集成选择,状态机 DSL 可复用现有的
ServiceInvoker和 TC 集群。
- 是 → Seata Saga 是最佳集成选择,状态机 DSL 可复用现有的
- 你的架构是否是纯事件驱动、多语言(Java/Go/Node.js)?
- 是 → Eventuate Tram 的编制模式与事件架构天然贴合,可跨语言订阅事件。
- 你的长事务中是否包含大量人工审批节点、需长期等待(数天)或需要给业务人员提供可视化流程追踪?
- 是 → Camunda BPMN 是唯一选择,其人工任务管理和流程监控能力无可替代。
7. Saga 与 TCC/AT/XA 的本质差异(关联第 1-3 篇)
将 Saga 放在整个分布式事务的图谱中,与前文所述的 XA、AT、TCC 进行对比,可以清晰地看到 Saga 的定位。
| 特性 | XA (2PC) | AT (Seata) | TCC | Saga |
|---|---|---|---|---|
| 锁持有时间 | 全程持有,直到 Phase 2 结束 | Phase 1 结束即释放本地锁,全局锁持有到 Phase 2 | Try 阶段结束后释放业务锁 | 无锁,每个 Ti 立即提交 |
| 隔离性 | 最强(Read Committed,取决于实现) | 中等(通过全局锁保证,见第2篇) | 中等偏弱(Try 预留资源提供隔离,见第3篇) | 最弱(无隔离,依赖语义锁) |
| 业务侵入性 | 低(但对 RM 有强要求) | 极低(只需 DataSourceProxy 和 undo_log 表) | 极高(需实现 Try/Confirm/Cancel 三接口及防悬挂等) | 中等(需手写 Ti 和 Ci 逻辑) |
| 补偿性质 | 资源管理器自动回滚(基于 Undo/Redo 日志) | 自动生成并执行行级补偿(基于 undo_log) | 业务级补偿(Cancel 接口,手动编写) | 业务级补偿(Ci 接口,手动编写) |
| 适用事务时长 | 秒级(锁竞争强度大) | 秒到几十秒(全局锁占用时间) | 秒级(资源预留不宜过长) | 分钟~小时级(无锁,适合长事务) |
| 并发性能 | 低 | 中等(全局锁瓶颈) | 较高 | 高 |
| 一致性模型 | 强一致性 | 最终一致性(自动补偿) | 最终一致性(业务补偿) | 最终一致性(业务补偿) |
| 典型场景 | 传统单体应用强一致事务 | 单服务多数据源、简单跨库操作 | 资金转账、库存操作等需要前置资源预留 | 跨国汇款、旅行预订、供应链履约 |
锁持有时间与隔离性四维对比图
图说明:
- 坐标轴:X 轴为锁持有时间(从左到右逐渐变长),Y 轴为隔离性(从下到上逐渐变强)。
- 位置解读:XA 位于右上角,代表持锁时间最长(全程),隔离性也最强;AT 通过全局锁增强了隔离性,持锁时间略短于 XA;TCC 通过 Try 阶段的快速预留,进一步缩短了锁持有时间,但隔离性略低于 AT;Saga 位于左下角,无锁、无预留,隔离性最弱,但事务持续时间可以极长。这恰恰是 Saga 成为长事务唯一解法的根本原因:为了换取时间上的极度自由,它完全放弃了自动隔离保障。
8. 面试高频专题
1. Saga 模式的核心原理是什么?为什么它适合长事务场景?
- 一句话回答:Saga 将长事务拆分为一系列立即提交的本地事务,并通过逆序补偿链回滚,从而避免在长时间跨度内持有锁。
- 详细解释:Saga 将一个全局事务拆分为多个有序的本地事务
T1..Tn,每个 Ti 执行后立即提交释放锁。若某个 Tk 失败,Saga 会逆序调用已完成 Ti 的补偿C(k-1)..C1来撤销业务影响。由于无锁,事务的持续时间可以长达分钟甚至小时,这是 XA 或 TCC 无法做到的(它们必须在事务期间持有锁或预留资源)。 - 多角度追问:
- 为什么 Saga 无需 Try 预留阶段?(因为 Ti 立即提交,资源直接变更,无需预留。)
- Saga 的隔离性缺陷是什么?(Ti 提交后,其他事务能读到未完成 Saga 的中间状态,需通过语义锁如状态字段防御。)
- 补偿 Ci 如果执行失败怎么办?(自动重试,耗尽后人工介入或向前恢复。)
- 加分回答:Saga 在 1987 年被提出,原文名为“Sagas”,其补偿思想深刻影响了后来所有的分布式事务补偿模型。其数学本质是一个可撤销的操作序列。
2. Saga 的 Choreography 和 Orchestration 两种协调模式有何本质区别?各适合什么场景?
- 一句话回答:Choreography 是无中心的事件驱动,服务松耦合但流程难追踪;Orchestration 是中心化的状态机驱动,易于监控但协调器是耦合点。
- 详细解释:Choreography 中,各服务通过发布/订阅领域事件相互触发,没有全局协调器,类似于舞者之间通过默契配合。Orchestration 则引入一个中心的 Saga 协调器(如状态机引擎),它命令式地调用每个服务并管理状态转换,类似于指挥官指挥乐队。前者适合事件驱动架构、多语言环境;后者适合需要强管控、易追溯的微服务体系。
- 多角度追问:
- 在编制模式下,如何知道整个 Saga 已经结束?(需监听最后一个事件,或者引入一个外部的追踪服务。)
- 编排模式下,如果协调器宕机了怎么办?(Seata Saga 等引擎会将状态持久化到数据库,恢复后从断点重试。)
- 哪种模式更容易添加一个新步骤?(编制模式只需添加事件监听器,对现有服务零侵入;编排模式需修改状态机定义并部署协调器。)
- 加分回答:在复杂业务中,常采用“混合模式”:核心交易链路用 Orchestration 做中心化管控,非核心的异步通知用 Choreography 实现。
3. Saga 的补偿链为什么必须逆序执行?如果正序补偿会有什么问题?
- 一句话回答:因为后执行的步骤依赖前一步骤的结果,逆序补偿才能保证数据依赖的正确性,正序补偿可能导致依赖数据已被破坏。
- 详细解释:假设
T1: 创建订单 → T2: 根据订单扣库存。如果 T2 失败后正序补偿C1(取消订单) → C2(恢复库存),C2 恢复库存时可能因找不到订单 ID 而无法计算正确的库存量。逆序补偿则先C2(恢复库存),此时订单仍存在,数据完整;再C1(取消订单),逻辑通顺。 - 多角度追问:
- 如果两步之间完全没有数据依赖,可以正序补偿吗?(逻辑上可以,但工程上为保持一致性和可维护性,应统一采用逆序。)
- Seata Saga 是如何保证逆序补偿的?(它维护了正向执行栈,失败后从栈顶依次弹出并执行其
CompensateState。) - TCC 的 Cancel 需要逆序吗?(一般也需要,同样是基于数据依赖的考虑,但 TCC 的协调者通常也是这样实现的。)
- 加分回答:在向前恢复(Forward Recovery)策略中,由于不是补偿而是重试或修复,其执行顺序可能是正向的,但那是另一套业务逻辑。
4. Saga 的 Ci 为什么必须幂等?如何保证 Ci 的幂等性?
- 一句话回答:Ci 可能因网络重试被调用多次,幂等性确保多次补偿不会产生额外副作用(如重复退款)。通常通过业务唯一键和状态机保证。
- 详细解释:补偿操作通常通过 RPC 调用,网络不可靠可能导致协调器重复发送补偿请求。幂等性保证执行一次和多次的结果相同。实现方式:在补偿的业务表中使用
bizId + actionType作为唯一键,先插入一条状态为PROCESSING的记录,若唯一键冲突则检查状态,若已COMPENSATED则直接返回成功,从而防止重复扣款或重复解锁库存。 - 多角度追问:
- 如果补偿操作是更新状态(如订单状态从
CONFIRMED改为CANCELLED),天然幂等吗?(是,多次更新为同一个状态具有幂等性。) - 如果补偿是调用外部支付系统的退款接口,如何保证幂等?(传入唯一的退款单号,下游支付系统根据退款单号防重。)
- 为什么 Ti 不需要强制幂等?(因为协调器通常只会成功调用一次 Ti,但若协调器自己重试,Ti 也需幂等,但这属于接口设计最佳实践。)
- 如果补偿操作是更新状态(如订单状态从
- 加分回答:Hmily TCC 框架通过
HmilyTransactionExecutor的本地事务日志来保证 Confirm/Cancel 的幂等,Saga 的 Ci 幂等设计思想与此同源。
5. Saga 与 TCC 的核心区别是什么?为什么 Saga 的隔离性比 TCC 弱?
- 一句话回答:TCC 有 Try 预留资源阶段,提供事务间隔离;Saga 无 Try,Ti 直接提交,隔离性最弱。
- 详细解释:TCC 的 Try 阶段会将资源(如库存、资金)冻结到一个中间状态,其他事务读到的是冻结后的剩余可用量,避免了脏读。Saga 的 Ti 则直接扣减,在事务最终成功或补偿前,其他事务会看到真实扣减后的数据,存在脏读。因此 Saga 的隔离性远弱于 TCC,但业务侵入性也更低(无需设计 Try 冻结逻辑)。
- 多角度追问:
- 能否在 Saga 的 Ti 里手动模拟一个“Try”冻结字段?(可以,这实际上就是加上了业务层的语义锁,代价是增加了侵入性。)
- 为什么资金转账场景通常用 TCC 而不是 Saga?(因为资金操作对隔离性要求极高,不能出现资金短暂消失的情况。)
- 为何航空公司超售场景可以用 Saga?(因为预订座位时短暂超售后,可通过后续的 Ci 取消预订或向前恢复协商解决,隔离性要求相对低。)
- 加分回答:Saga 可以看作是 TCC 的“退化”版本,去掉了 Try 阶段,从而获得了对长事务时间跨度的支持,但牺牲了隔离性。这是典型的架构取舍(Trade-off)。
6. Seata Saga 的状态机 DSL 是如何定义的?ServiceTask 和 CompensationTask 分别对应什么?
- 一句话回答:通过 JSON/YAML 定义,
ServiceTask定义正向服务调用 (Ti),CompensateState属性绑定对应的补偿任务 (Ci)。 - 详细解释:状态机 DSL 由
States和Transitions组成。ServiceTask类型的状态代表一个正向操作,需指定ServiceName、ServiceMethod和CompensateState。当ServiceTask成功执行后,引擎会将其入栈;失败时,引擎会从栈中弹出已成功的ServiceTask,并跳转到它们指定的CompensateState执行补偿。补偿状态本身也是ServiceTask类型。 - 多角度追问:
Choice状态是什么?(是条件分支,根据 SpEL 表达式的结果决定跳转到哪个后续状态。)Retry配置是加在ServiceTask上还是全局的?(是加在ServiceTask上的,允许为每个步骤定义不同的重试策略。)- 状态机实例的状态是如何持久化的?(由
StateMachineInstanceStore实现,默认有 MySQL 等实现,写入state_machine_inst和state_inst表。)
- 加分回答:Seata Saga 的 DSL 设计灵感来源于 AWS Step Functions,用声明式的 JSON 来描述流程,降低了编排逻辑的复杂度。
7. Eventuate Tram 的 @SagaOrchestrator 和 @SagaCompensatingHandler 是如何配合工作的?
- 一句话回答:
@SagaOrchestrator定义 Saga 流程,@SagaCommandHandler处理正向命令,@SagaCompensatingHandler绑定补偿命令;框架根据命令回复事件自动逆序调用补偿处理器。 - 详细解释:在 Eventuate Tram 中,你定义一个 Saga 接口并用
@SagaOrchestrator注解。接口中的方法通过@SagaCommandHandler定义 Saga 的一个步骤(发送一个命令并期待回复事件)。在同一个接口中,你可以定义补偿方法,并用@SagaCompensatingHandler(method = "正向方法名")来绑定。当后续步骤的回复是失败事件时,框架会自动生成补偿命令,并调用对应method的CompensatingHandler,实现逆序补偿。 - 多角度追问:
- Eventuate Tram 使用什么消息队列?(原生支持 Kafka,也支持 RabbitMQ 和 ActiveMQ。)
- 如果补偿处理器也失败了怎么办?(框架会重复发送补偿命令,若最终失败,事件会被放入死信队列,需人工处理。)
- 这与纯粹的 Choreography 有何不同?(Eventuate Tram 虽然事件驱动,但 Saga 的定义是集中的,类似一个分布式的编排器,结合了两种模式的特点。)
- 加分回答:Eventuate Tram 的核心在于把“命令”和“事件”作为一等公民,通过事件溯源(Event Sourcing)的思想来管理 Saga 状态,这为构建 CQRS/Event Sourcing 架构提供了天然支持。
8. Saga 的补偿失败如何处理?什么是向前恢复?
- 一句话回答:补偿失败通过“自动重试 -> 人工介入”的流程处理;向前恢复是在补偿代价太大时,执行新步骤将系统推向一个可接受的终态,而非逆序回滚。
- 详细解释:Ci 失败后,引擎/框架会进行指数退避的重试。若重试耗尽,会持久化失败记录并告警。人工介入通过后台任务进行手动重试或跳过。向前恢复(Forward Recovery)是补偿之外的另一条路:例如订单已发货,取消订单的 Ci 不切实际,此时可执行“通知物流召回”或“协商退货”等正向步骤。
- 多角度追问:
- 向前恢复和重试有什么区别?(重试是重复执行失败的 Ci,向前恢复是执行一个完全不同的新步骤。)
- 如何选择是补偿还是向前恢复?(取决于业务成本。补偿成本低则补偿;若补偿代价高昂或不可能,则设计向前恢复流程。)
- 人工介入的 Admin 后台通常记录什么信息?(记录 Saga ID、失败步骤、业务主键、错误堆栈、重试次数,并提供重试/跳过按钮。)
- 加分回答:在微服务实践中,对于 Ci 失败的作业,可以将其视为一种“业务告警”,推动产品层面设计“异常单”处理流程,由运营团队在后台完成修复。
9. Saga 与 Seata AT 的补偿机制有何不同?各自适合什么场景?
- 一句话回答:AT 的补偿是由 TC 根据
undo_log自动生成的行级逆向 SQL,完全透明;Saga 的补偿是开发者手写的业务级 Ci 方法,需要手动编码。 - 详细解释:AT 模式要求每个 RM 都托管
DataSourceProxy,由它自动在undo_log表记录数据前后的镜像。回滚时 TC 发送 branch rollback 请求,RM 自动解析undo_log生成反向 SQL 执行补偿(详见第2篇)。Saga 则完全不限制 RM 的类型(可以是任何微服务),但开发者必须在业务代码中手动实现 Ci。AT 适合简单的 CRUD 跨库事务;Saga 适合调用非数据库资源或需聚合多个操作的长事务。 - 多角度追问:
- AT 的
undo_log是在什么时候插入的?(在 Phase 1 提交本地事务之前,与业务 SQL 在同一个本地事务中。) - Saga 的 Ci 可以调用非数据库操作吗?(当然,比如调用退款 API、发邮件通知等,这是 AT 无法做到的。)
- 为什么 AT 不适合长事务?(因为全局锁在 Phase 2 结束前一直持有,长事务会导致长锁,引发严重的锁竞争和死锁。)
- AT 的
- 加分回答:AT 的自动补偿虽方便,但对 SQL 类型和数据库版本有兼容性要求;Saga 的手动补偿更灵活但开发量大。两者体现了“自动挡”和“手动挡”的设计哲学。
10. Camunda BPMN 如何实现 Saga?Compensation Boundary Event 起什么作用?
- 一句话回答:Camunda 通过 BPMN 2.0 流程建模定义 Saga,
Compensation Boundary Event绑定在ServiceTask上,作为该任务失败或被中断时的补偿操作。 - 详细解释:在 Camunda 中,将一个
ServiceTask视为 Ti,其边界上的Compensation Boundary Event就是 Ci。流程引擎在补偿时,会逆序遍历已完成的ServiceTask,并触发其补偿事件。这使得 Saga 的补偿链直接映射为可视化的流程图节点,非开发人员也能理解业务流程的补偿逻辑。 - 多角度追问:
- Camunda 如何保证补偿的逆序性?(BPMN 规范本身就定义了逆序补偿的语义,引擎基于
ActivityStack实现。) - 人工审批节点(UserTask)可以有补偿吗?(通常 UserTask 代表一个决策,它没有直接的补偿操作,但其上游的 ServiceTask 有补偿。)
- Camunda 的 Saga 和 Seata Saga 能结合吗?(可以,Seata Saga 作为 Camunda 的外部任务,Camunda 负责整体流程和人工节点,Seata Saga 负责托管具体的微服务调用事务。)
- Camunda 如何保证补偿的逆序性?(BPMN 规范本身就定义了逆序补偿的语义,引擎基于
- 加分回答:利用 Camunda 实现 Saga,最大的优势是使“事务流程”成为可讨论、可评审、可优化的公司资产,而不仅仅是开发者的注释或配置。
11. 在 Saga 中,如果 C2 执行失败,C1 是否还会执行?为什么?
- 一句话回答:不会立即执行。Saga 协调器会停住并重试 C2,直到 C2 成功,才会继续执行 C1。
- 详细解释:Saga 的补偿链是严格顺序且原子化的:
C2 -> C1。C2 必须成功,整个补偿链才能继续向前推进。如果 C2 失败后跳过它直接执行 C1,会导致数据严重不一致。协调器会不断重试 C2,若重试耗尽则整个 Saga 陷入失败等待态(中间不一致),此状态需要通过人工介入或向前恢复来修复。一旦人工解决 C2 并标记为已补偿,协调器会继续执行 C1。 - 多角度追问:
- 如果 C2 失败了 100 次,业务上该怎么办?(触发告警,值班人员登录 Admin 后台,查看 C2 失败原因,手动修复后重试 C2 或跳过。)
- 能否配置为 C2 失败后跳过 C2 直接执行 C1?(绝不可以,这会破坏数据一致性的核心保证,是架构腐化的开始。)
- 如果 C1 依赖 C2 成功才能执行怎么办?(这正是逆序补偿的精髓:补偿链的顺序保证了数据依赖的正确性。)
- 加分回答:补偿链的这种“不可跳过”特性,是 Saga 模式在理论上能达到“最终一致性”的基石,也是它与“最大努力通知”的根本区别。
12. Saga 如何保证数据最终一致性?与强一致性事务(2PC)有何本质区别?
- 一句话回答:Saga 通过业务补偿将系统推向最终一致状态,期间存在不一致窗口;2PC 通过资源管理器锁在提交瞬间保证原子性和一致性,无中间不一致窗口。
- 详细解释:2PC 要求所有参与者在 Phase 1 投票,Phase 2 提交,期间所有资源被锁定,事务结束时数据瞬间一致。Saga 拆分为多个独立提交的本地事务,若某步失败,通过逆序补偿将已提交的数据“业务撤销”,最终达到一致,但补偿执行前存在中间不一致状态。Saga 是最终一致性,2PC 是强一致性。
- 多角度追问:
- 最终一致性的时间窗口可能有多长?(取决于重试策略和故障恢复速度,可能从几秒到数分钟。)
- 用户如何感知最终一致性?(通常通过 UI 状态屏蔽,例如显示“处理中”,直到 Saga 成功或失败。)
- Saga 能否保证读己之写(Read Your Writes)?(很难,因为不同步骤可能分散在不同的数据库,查询可能看到中间态。)
- 加分回答:Saga 的最终一致性是分布式系统 CAP 原则的体现:它牺牲了强一致性(C)和隔离性(部分 A),以换取高可用和无限的时间跨度。
13. Seata Saga 的重试机制是如何实现的?与 Spring Retry 有何异同?
- 一句话回答:Seata Saga 在引擎层通过
RetryInterceptor实现重试,支持指数退避和针对特定异常;Spring Retry 是独立的工具库,可应用于任何方法,但无 Saga 上下文。 - 详细解释:在状态机 DSL 中配置
Retry后,引擎执行ServiceTask时会用RetryInterceptor包装调用。该拦截器根据配置的Exceptions、IntervalSeconds、BackoffRate和MaxAttempts,在方法抛出匹配异常时进行重试,重试间隔通过Thread.sleep或定时任务实现。与 Spring Retry 相比,Seata Saga 的重试与状态机实例的生命周期绑定,重试信息会持久化到state_inst表中,确保引擎重启后能继续重试。 - 多角度追问:
- 重试过程中状态机实例的状态是什么?(仍为
RU运行中,当前状态步骤为RETRYING。) - 如果重试期间整个服务崩溃,引擎恢复后如何继续?(重新加载状态机实例,根据
state_inst中的步骤状态继续重试或执行下一动作。) - 可以配置全局的重试策略吗?(状态机 DSL 支持在状态机级别定义默认重试,步骤可覆盖。)
- 重试过程中状态机实例的状态是什么?(仍为
- 加分回答:Seata Saga 的重试本质上是“执行-失败-等待”循环,它利用了状态机引擎的持久化与断点续跑能力,实现了有状态的重试,比无状态的 Spring Retry 更适合长事务场景。
14. 如何监控和追踪 Saga 的执行状态?
- 一句话回答:通过查询 Seata Saga 的
state_machine_inst和state_inst表,或集成 Metrics(如 Prometheus)暴露指标;Eventuate Tram 则需聚合事件日志;Camunda 使用内置 Cockpit。 - 详细解释:在生产中,我们需要知道当前有多少 Saga 在运行、失败数量、补偿链执行情况等。Seata Saga 提供了数据库表,可直接查询或暴露 SQL 查询为 Metrics。例如,每隔 30 秒统计
status='RU'的数量作为活跃 Saga 指标,status='FA'作为失败告警。还可以通过扩展StateMachineEngine的事件监听器,在关键节点发送 Metrics 事件。Camunda 的 Cockpit 提供开箱即用的图形化监控,Eventuate Tram 则需要自定义实现,通常基于 Kafka Streams 或 Elasticsearch 聚合事件。 - 多角度追问:
- 如何实现 Saga 执行时间的监控?(在
state_machine_inst表中记录gmt_started和gmt_end,计算差值。) - 如果 Saga 卡住超过一定时间怎么办?(通过定时任务扫描
status='RU'且gmt_updated超过阈值(如 30 分钟)的记录,发出告警。) - 如何追踪具体业务数据?(将业务主键存入
start_params,与日志系统(ELK)关联,通过 Saga ID 串联所有调用日志。)
- 如何实现 Saga 执行时间的监控?(在
- 加分回答:Seata 官方提供了 Saga 监控的示例,可基于 Prometheus + Grafana 搭建监控大盘,展示 Saga 执行成功率、平均执行时间、补偿链触发次数等核心指标。
15. (系统设计题)跨国汇款系统 题目:一个跨国汇款系统需要在 5-30 分钟内完成:扣款(账户服务A)→ 换汇(外汇服务B)→ 入账(收款账户服务C),任一步骤失败需回滚整个流程。请给出: (1)为什么 XA/AT/TCC 不适合此场景? (2)Saga 方案的正向步骤 Ti 与补偿步骤 Ci 的详细设计。 (3)Orchestration 编排的状态机 DSL 定义(含重试策略与超时配置)。 (4)补偿失败的监控告警与人工介入方案。 (5)如何通过语义锁防止用户在汇款中途重复提交。
参考答案:
- (1)模式分析:
- XA:需要跨银行、跨外汇系统持有数据库锁 5-30 分钟,这在现实世界中完全不可行,性能为零。
- AT:同上,虽然锁粒度更细,但长事务的全局锁问题依然致命。
- TCC:Try 阶段需“冻结”账户资金和外汇头寸长达 30 分钟,银行不可能允许如此长时间的资金冻结,业务上完全不可接受。
- 结论:Saga 是唯一选择,因为它在每个步骤完成后立即提交,不持有任何锁或预留资源。
- (2)Ti 与 Ci 设计:
T1 (扣款):调用账户服务A,从用户A账户扣除 USD 1000。补偿C1 (退款):调用账户服务A,向用户A账户退回 USD 1000,退款单号bizId-refund。T2 (换汇):调用外汇服务B,按照实时汇率将 USD 1000 兑换为 CNY。补偿C2 (反向换汇):调用外汇服务B,将兑换后的 CNY 按原汇率或市价换回 USD。T3 (入账):调用账户服务C,将 CNY 计入收款人账户。补偿C3 (冲正):调用账户服务C,从收款人账户扣回 CNY。
- (3)状态机 DSL(关键片段):
{ "Name": "CrossBorderTransferSaga", "StartState": "DebitAccount", "States": { "DebitAccount": { "Type": "ServiceTask", "ServiceName": "accountServiceA", "ServiceMethod": "debit", "CompensateState": "RefundAccount", "Next": "ExchangeFx", "Input": ["$.transferId", "$.amount"], "Status": { "#root.success" }, "Retry": [ { "Exceptions": ["TimeoutException"], "IntervalSeconds": 5, "MaxAttempts": 3 } ] }, "ExchangeFx": { "Type": "ServiceTask", "ServiceName": "fxService", "ServiceMethod": "exchange", "CompensateState": "ReverseFx", "Next": "CreditAccount", "Input": ["$.transferId", "$.sourceAmount", "$.sourceCurrency", "$.targetCurrency"], "Retry": [ { "Exceptions": ["*"], "IntervalSeconds": 10, "MaxAttempts": 5, "BackoffRate": 2.0 } ] }, "CreditAccount": { "Type": "ServiceTask", "ServiceName": "accountServiceC", "ServiceMethod": "credit", "CompensateState": "ReverseCredit", "Next": "Succeed", "Retry": [ { "Exceptions": ["TimeoutException"], "IntervalSeconds": 5, "MaxAttempts": 3 } ] }, "RefundAccount": { "Type": "ServiceTask", "ServiceName": "accountServiceA", "ServiceMethod": "refund", "Next": "Fail" }, "ReverseFx": { "Type": "ServiceTask", "ServiceName": "fxService", "ServiceMethod": "reverse", "Next": "RefundAccount" }, "ReverseCredit": { "Type": "ServiceTask", "ServiceName": "accountServiceC", "ServiceMethod": "reverseCredit", "Next": "ReverseFx" }, "Succeed": { "Type": "Succeed" }, "Fail": { "Type": "Fail" } } } - (4)监控与人工介入:
- 监控:Prometheus 监控
saga_compensation_log表中status='FAILED'的增量,以及state_machine_inst中status='FA'的数量。 - 告警:当出现
FAILED记录且持续时间超过 15 分钟,发送 PagerDuty/钉钉告警。 - 人工后台:运维登录 Admin 系统,查看失败 Saga 的完整执行历史(每一步的输入/输出/异常),确认问题(如外汇服务宕机)并点击“重试补偿”按钮。若问题无法立即修复,可点击“标记为待处理”,并与业务方协商。
- 监控:Prometheus 监控
- (5)语义锁防重:
- 在汇款发起请求的入口表中,增加状态字段
status,初始为INIT。 - 汇款 Saga 的
T1(扣款)执行前,先通过乐观锁UPDATE transfer SET status='PROCESSING' WHERE id=123 AND status='INIT'。若返回影响行数为 0,则说明重复提交,直接返回“处理中/已提交”,避免重复扣款。 - 若 Saga 最终成功,更新为
SUCCESS;若失败补偿完成,更新为FAILED。用户在页面上能看到明确的终态。
- 在汇款发起请求的入口表中,增加状态字段
Saga 核心机制速查表
| 项目 | 机制/答案 |
|---|---|
| 协调模式 | Orchestration(中心状态机驱动,如 Seata/Camunda);Choreography(事件驱动,如 Eventuate Tram) |
| 核心组件 | 状态机引擎 (Seata)、事件编排器 (Eventuate)、流程引擎 (Camunda)、补偿管理器、状态持久化 (DB) |
| 补偿链顺序 | 严格逆序:T1, T2, T3 失败 -> 补偿 C2, C1 |
| Ci 设计原则 | 幂等性(唯一键)、最终成功(重试)、上下文传递(Saga 上下文或业务表) |
| 隔离性缺陷 | Ti 立即提交,无隔离,需用语义锁(业务状态字段)防御 |
| 与 TCC 本质区别 | TCC 有 Try 预留资源(隔离性较强,侵入性高);Saga 无 Try(隔离性最弱,侵入性中) |
| 与 AT 本质区别 | AT 通过 undo_log 自动生成行级补偿;Saga 手动编写业务级补偿 Ci |
| 补偿失败策略 | 1. 自动重试(指数退避) 2. 人工介入(Admin 后台) 3. 向前恢复 |
| 代表框架 | Seata Saga(DSL 编排)、Eventuate Tram(事件编制)、Camunda(BPMN 编排) |
| 适用场景 | 跨国汇款(5-30min)、旅行预订(分钟级)、供应链履约(分钟~天级)、含人工审批流程 |
延伸阅读
- Seata 官方文档 - Saga 模式:seata.io/zh-cn/docs/…
- Eventuate Tram 官方文档 - Saga Orchestrator:eventuate.io/docs/manual…
- Camunda BPMN 文档 - Compensation Event:docs.camunda.org/manual/7.20…
- Chris Richardson. Microservices Patterns. Manning Publications, 2018. (Saga 章节系统阐述了微服务下 Saga 的协调与补偿)
- Martin Kleppmann. Designing Data-Intensive Applications (DDIA). O'Reilly Media, 2017. (第 7 章深入讨论事务、隔离与补偿)