微服务中的 Saga 模式实战:订单/库存/支付场景的补偿设计
提出问题
分布式系统中,一个下单操作需要跨订单服务、库存服务、支付服务、积分服务协同完成。如果库存扣减成功但支付失败,或者支付成功但积分发放失败,如何保证数据最终一致?传统数据库的 ACID 事务在跨服务场景下无法使用(2PC 性能差、阻塞时间长),业务上必须接受"中间状态"。Saga 模式就是解决这个问题的标准方案——它将一个大事务拆分为多个本地事务,每个本地事务都有对应的补偿操作,当某个步骤失败时,按相反顺序执行补偿来撤销已执行的操作。
面试官问 Saga 时,通常不是在考你知不知道 Choreography 和 Orchestration 两种模式的区别,而是在考察你对补偿设计、幂等性、隔离性的实战理解。生产上 Saga 最容易踩坑的地方不是"选哪种模式",而是"补偿操作怎么保证最终成功"、"中间状态如何被其他事务感知"、"补偿悬挂怎么处理"。
分析问题
Saga 核心拆解:本地事务 + 补偿操作
Saga 模式的核心思想很简单:把分布式事务拆成一串本地事务,每个本地事务配一个"反操作"(补偿)。以下单流程为例:
订单服务(创建订单) → 库存服务(扣减库存) → 支付服务(扣款) → 积分服务(发积分)每个步骤单独提交本地事务,成功后执行下一步。如果某一步失败(比如支付扣款失败),就按相反顺序执行补偿:
积分补偿(取消积分) → 支付补偿(退款) → 库存补偿(加回库存) → 订单补偿(取消订单)关键点:补偿操作也是本地事务,它本身也可能失败。所以补偿操作必须支持重试,直到成功或达到最大重试次数后人工介入。
// Saga 协调器的状态机实现(简化版)
public class SagaOrchestrator {
private final SagaStateRepository stateRepo;
public SagaResult executeOrderSaga(OrderRequest request) {
String sagaId = UUID.randomUUID().toString();
stateRepo.save(new SagaState(sagaId, SagaStatus.STARTED));
try {
// 步骤 1: 创建订单
stepWithRetry(sagaId, 1, () -> orderService.createOrder(request));
stateRepo.updateStep(sagaId, 1, StepStatus.SUCCESS);
// 步骤 2: 扣减库存
stepWithRetry(sagaId, 2, () -> inventoryService.deduct(request.getSkuId(), request.getQuantity()));
stateRepo.updateStep(sagaId, 2, StepStatus.SUCCESS);
// 步骤 3: 扣款
stepWithRetry(sagaId, 3, () -> paymentService.charge(request.getUserId(), request.getAmount()));
stateRepo.updateStep(sagaId, 3, StepStatus.SUCCESS);
// 步骤 4: 发积分
stepWithRetry(sagaId, 4, () -> pointService.grant(request.getUserId(), request.getAmount()));
stateRepo.updateStep(sagaId, 4, StepStatus.SUCCESS);
stateRepo.updateStatus(sagaId, SagaStatus.COMPLETED);
return SagaResult.success(sagaId);
} catch (Exception e) {
// 失败时触发补偿,按相反顺序
compensate(sagaId, 4); // 先补偿积分
compensate(sagaId, 3); // 再退款
compensate(sagaId, 2); // 加回库存
compensate(sagaId, 1); // 取消订单
stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATED);
return SagaResult.failed(sagaId, e.getMessage());
}
}
private void stepWithRetry(String sagaId, int step, Runnable action) {
// 幂等检查:如果该步骤已成功执行,跳过
StepStatus status = stateRepo.getStepStatus(sagaId, step);
if (status == StepStatus.SUCCESS) return;
// 带重试的执行(最多 3 次,指数退避)
RetryUtils.retry(3, 100, action);
}
private void compensate(String sagaId, int step) {
// 补偿操作的幂等重试
RetryUtils.retry(5, 200, () -> {
// 根据 step 调用对应的补偿接口
stateRepo.executeCompensation(sagaId, step);
});
}
}完整时序流程
为了理解 Saga 的补偿时序,这里用文字描述一次完整的正常流程和补偿流程:
正常流程时序:
OrderService.createOrder (成功)
→ 提交本地事务,订单状态=CREATED
→ 通知 Orchestrator: step1 OK
InventoryService.deductStock (成功)
→ 提交本地事务,库存扣减+1
→ 通知 Orchestrator: step2 OK
PaymentService.charge (成功)
→ 提交本地事务,扣款 100 元
→ 通知 Orchestrator: step3 OK
PointService.grant (成功)
→ 提交本地事务,积分+100
→ 通知 Orchestrator: step4 OK
Orchestrator: 标记 Saga COMPLETED补偿流程时序(step3 扣款失败):
OrderService.createOrder (成功)
InventoryService.deductStock (成功)
PaymentService.charge (失败,余额不足)
→ 抛异常,Orchestrator catch
→ Orchestrator: 标记 Saga COMPENSATING
PointService.compensateGrant (补偿:扣回积分)
→ 本步骤未执行过,跳过(幂等检查)
PaymentService.compensateCharge (补偿:退款)
→ 本步骤未扣款成功,跳过(幂等检查)
InventoryService.compensateDeduct (补偿:加回库存)
→ 执行:INVENTORY_TABLE.stock_quantity += 1
→ 记录补偿记录:compensation_records(saga_id, 'deduct')
OrderService.compensateCreate (补偿:取消订单)
→ 执行:ORDERS_TABLE.status = 'CANCELLED'
→ 记录补偿记录:compensation_records(saga_id, 'create')
Orchestrator: 标记 Saga COMPENSATEDChoreography vs Orchestration:选型不是二分法
两种实现方式各有适用场景,关键在于业务流程的复杂度和变更频率。
Choreography(编排模式):没有中心协调器,每个服务执行完本地事务后通过 MQ 发送事件触发下一个服务。订单服务发"订单已创建"事件 → 库存服务订阅后扣库存,发"库存已扣减"事件 → 支付服务订阅后扣款...
// Choreography 模式:订单服务发送事件
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 本地事务创建订单
Order order = orderRepository.save(Order.createPending(request));
// 2. 发送事件触发下一步(库存扣减)
eventPublisher.publish(new OrderCreatedEvent(
order.getId(), request.getSkuId(), request.getQuantity()
));
return order;
}
}优点:服务间完全解耦,新增服务不用改已有代码。缺点:业务逻辑分散在多个服务的消息处理器中,一个流程跨 5 个服务,排错时需要看 5 套日志。最致命的坑:消息顺序不可控——如果积分服务先收到"支付成功"事件,再收到"库存扣减成功"事件,可能导致积分发放了但库存没扣减。
Orchestration(协调器模式):引入一个 Saga 协调器,负责编排整个流程。协调器通过状态机管理 Saga 实例状态,每个步骤完成后记录到数据库,宕机后重启能从持久化状态恢复。
// Orchestrator 的状态机持久化
@Entity
@Table(name = "saga_state")
public class SagaState {
@Id
private String sagaId;
@Enumerated(EnumType.STRING)
private SagaStatus status; // STARTED / COMPLETED / COMPENSATING / COMPENSATED
@ElementCollection
@CollectionTable(name = "saga_steps")
private List<StepRecord> steps; // 每个步骤的执行状态
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// 重启恢复:扫描所有未完成的 Saga
@Component
public class SagaRecoveryJob {
@Scheduled(fixedDelay = 30_000)
public void recoverIncompleteSagas() {
List<SagaState> incomplete = sagaRepo.findByStatusIn(
List.of(SagaStatus.STARTED, SagaStatus.COMPENSATING)
);
for (SagaState saga : incomplete) {
sagaOrchestrator.resume(saga);
}
}
}选型建议:3-5 个服务、流程简单、变更频率低 → Choreography(轻量)。5-10 个服务、流程复杂、需要可观测 → Orchestration(可控)。生产环境多数团队最终会走向 Orchestration,因为可追踪、可恢复、可调试。
隔离性(Isolation)问题:Saga 最大的坑
Saga 不提供 ACID 中的隔离性——一个 Saga 执行过程中,其他 Saga 可能会看到中间状态(脏读)。这是面试中 P7 级必问的深度点。
场景:用户 A 下单 100 元,扣库存和扣款两个 Saga 同时执行。
Saga-1: 订单创建 → 库存扣减(成功) → 支付扣款(进行中)
Saga-2: 订单创建 → 库存查询(看到 Saga-1 已扣减,库存不足) → 失败如果 Saga-1 最终支付失败触发补偿,库存加回,但 Saga-2 已经因为"库存不足"而失败返回了——Saga-2 被 Saga-1 的中间状态误导了。
更真实的踩坑案例:某电商平台双十一期间,库存服务和支付服务之间存在 200ms 的延迟。Saga 协调器在扣库存成功后立即调用支付,但支付超时触发补偿取消订单。此时另一个用户在同一秒查询库存,发现库存已被释放(补偿生效),下单成功——但原 Saga 的支付重试请求恰好延迟到达,导致一笔扣款成功但订单已取消。这就是"补偿和后续请求时序交错"的经典问题。
解决方案:
- 语义锁(Semantic Lock):在资源上加状态标记。订单状态从
CREATING → PENDING → CONFIRMED,其他 Saga 看到PENDING状态时知道该资源正在被处理,不直接操作。
-- 语义锁:订单状态字段作为锁标记
UPDATE orders
SET status = 'PENDING', saga_id = 'saga-xxx'
WHERE id = ? AND status = 'CREATING';
-- 影响行数 = 0 说明已被其他 Saga 锁定冲正抵消(Countermeasure):允许中间状态的脏读,但通过补偿操作抵消副作用。适用于库存这类"最终一致即可"的场景——短时间内的超卖可以容忍,因为补偿操作会把库存加回来。
版本号乐观锁:每个资源带版本号,更新时检查版本号是否被其他 Saga 修改过。
-- 乐观锁版本号 + 补偿防止超卖
UPDATE inventory
SET stock_quantity = stock_quantity - 1, version = version + 1
WHERE sku_id = ? AND stock_quantity >= 1 AND version = ?;
-- 影响行数 = 0 说明被其他事务修改了,重试或放弃补偿悬挂(Compensation Hanging):比补偿失败更隐蔽的坑
补偿悬挂是指:正向操作实际上已经成功,但协调器因为超时判定为失败,于是执行补偿;但正向操作的成功消息事后才到达,导致补偿操作错误地撤销了一个实际上成功的操作。这是 Saga 模式中最容易踩的坑,比"补偿失败"隐蔽得多。
典型时序:
协调器 → 支付服务: charge(100元)
→ 支付服务处理成功,返回响应
→ 网络延迟 3 秒,协调器超时
协调器: 超时,标记支付失败
协调器 → 订单服务: compensateCreate(取消订单)
→ 订单取消成功
协调器: 收到支付服务的成功响应(延迟到达)
→ 钱已扣,但订单已取消 → 用户白付了 100 元落地解法:补偿操作执行前,必须查远端状态的当前快照,确认"我确实需要补偿"。
// 补偿前检查远端状态
public void compensateCreate(String sagaId, String orderId) {
// 1. 查远端状态
OrderStatus currentStatus = orderService.queryStatus(orderId);
if (currentStatus != OrderStatus.CREATED) {
// 订单已经被其他流程取消了,或者支付成功但订单状态不对
log.warn("补偿跳过,订单当前状态={},不是预期的 CREATED", currentStatus);
return;
}
// 2. 用乐观锁更新状态
int affected = orderRepo.updateStatus(orderId, OrderStatus.CANCELLED, OrderStatus.CREATED);
if (affected == 0) {
// 状态被并发修改了,重新检查
throw new ConcurrentModificationException("订单状态已被修改,需要重试补偿");
}
// 3. 记录补偿记录
compensationRepo.save(new CompensationRecord(sagaId, "create", LocalDateTime.now()));
}生产数据:某 O2O 平台上线 Saga 后,前三个月每月因补偿悬挂导致的数据不一致约 200-300 单(日均 7-10 单),占 Saga 总事务量的 0.05%。排查后发现根因全部是"补偿前未查远端状态"。加上远端状态检查后,补偿悬挂归零。
Saga vs TCC:面试官最爱问的对比
很多面试者会把 Saga 和 TCC(Try-Confirm-Cancel)混为一谈,或者只知道"Seata 支持 TCC 模式"这个结论。以下是两者的关键区别:
| 维度 | Saga | TCC |
|---|---|---|
| 事务粒度 | 整个 Saga 一个大事务 | 每个参与方一个 Try,各自预留资源 |
| 资源锁定 | 不锁定,直接操作资源 | Try 阶段预留资源(如冻结库存、冻结金额) |
| 隔离性 | 不提供,业务层处理 | Try 天然提供写隔离(预留资源被独占) |
| 补偿代价 | 高——补偿需要完全撤销操作 | 低——Cancel 释放预留资源即可 |
| 实现复杂度 | 低——只需写正向+补偿逻辑 | 高——每个服务需要写 Try/Confirm/Cancel 三套逻辑 |
| 适用场景 | 长事务、跨系统、异构 | 短事务、资源竞争激烈、要求高隔离性 |
| 典型框架 | 纯手工实现 | Seata TCC、Hmily |
| 性能 | 高(无锁) | 中等(资源预留有锁开销) |
选型判断:库存扣减这种资源竞争激烈的场景,TCC 的 Try 阶段冻结库存天然防超卖,比 Saga 的补偿方案更干净。但支付场景,TCC 的 Try 冻结金额需要银行接口支持预授权(authorization),很多第三方支付渠道不提供这个能力,只能走 Saga 的"先扣后退"。所以一个大型系统里往往是 Saga + TCC 混用——库存用 TCC Try 冻结,支付用 Saga 扣款。
补偿操作的幂等性设计
补偿操作可能因为网络超时、协调器重启等原因被重复调用。补偿必须幂等。
// 库存补偿(加回库存)的幂等实现
@Component
public class InventoryCompensationService {
@Autowired
private CompensationRecordRepo compensationRepo;
@Autowired
private InventoryRepo inventoryRepo;
@Transactional
public void compensateDeduct(String sagaId, String skuId, int quantity) {
// 幂等检查:去重表
if (compensationRepo.existsBySagaIdAndAction(sagaId, "deduct")) {
log.info("补偿已执行,跳过: sagaId={}, action=deduct", sagaId);
return;
}
// 执行补偿:加回库存
inventoryRepo.addStock(skuId, quantity);
// 记录补偿执行记录
compensationRepo.save(new CompensationRecord(sagaId, "deduct", LocalDateTime.now()));
}
}去重表设计:(saga_id, action) 作为联合唯一键,同一个 Saga 的同一个补偿操作只执行一次。注意:补偿记录和补偿操作必须在同一个本地事务中,否则补偿记录写入失败但补偿已执行,下次重试会重复补偿。
补偿操作的幂等还需要考虑正向操作的幂等
正向操作本身也可能被重复调用(协调器重试)。如果正向操作不是幂等的,重试会导致重复扣库存、重复扣款。
// 正向操作幂等:用 saga_id 作为幂等键
@Transactional
public void deduct(String sagaId, String skuId, int quantity) {
// 幂等检查
if (deductRecordRepo.existsBySagaId(sagaId)) {
log.info("库存扣减已执行,跳过: sagaId={}", sagaId);
return;
}
// 扣减库存
int affected = inventoryRepo.deductStock(skuId, quantity);
if (affected == 0) {
throw new InsufficientStockException(skuId, quantity);
}
// 记录扣减记录
deductRecordRepo.save(new DeductRecord(sagaId, skuId, quantity));
}注意:正向操作和补偿操作的幂等键不能冲突。正向用 (saga_id, "deduct"),补偿用 (saga_id, "compensate_deduct"),两个不同的键。
用 MQ 实现 Saga 的一件事特性
一个容易忽略的细节:Saga 的"正向操作 + 发送下一步事件"必须是原子操作。如果事务提交了但 MQ 消息没发出去,流程就断了。
// 本地消息表 + 定时任务,保证消息必达
@Service
public class EventualSender {
@Autowired
private EventMessageRepo eventRepo;
@Autowired
private KafkaTemplate<String, Object> kafka;
@Transactional
public void createOrderAndSendEvent(OrderRequest request) {
// 1. 本地事务创建订单
Order order = orderRepository.save(Order.createPending(request));
// 2. 本地事务写入消息表(同一事务)
EventMessage msg = new EventMessage(
UUID.randomUUID().toString(),
"ORDER_CREATED",
order.getId(),
LocalDateTime.now()
);
eventRepo.save(msg);
}
// 定时任务:扫描未发送的消息并发送
@Scheduled(fixedDelay = 1_000)
@Transactional
public void sendPendingMessages() {
List<EventMessage> pending = eventRepo.findTop100ByStatus(MessageStatus.UNSENT);
for (EventMessage msg : pending) {
try {
kafka.send(msg.getTopic(), msg.getPayload()).get(3, TimeUnit.SECONDS);
msg.setStatus(MessageStatus.SENT);
eventRepo.save(msg);
} catch (Exception e) {
log.warn("消息发送失败,下次重试: msgId={}", msg.getId());
}
}
}
}核心逻辑:订单创建和消息写入在同一个本地事务里,定时任务保证消息最终发出。这比用 @TransactionalEventListener(phase = AFTER_COMMIT) 靠谱,因为 MQ 宕机时 AFTER_COMMIT 回调不会重试。
Seata AT 模式 vs 纯 Saga:他们不是一个东西
很多面试者会把 Seata AT 和 Saga 混为一谈,这是扣分项。
| 维度 | 纯 Saga | Seata AT |
|---|---|---|
| 原理 | 业务方手动写补偿操作 | 代理自动生成逆向 SQL(UNDO_LOG) |
| 隔离性 | 不提供,需业务层自己处理 | 读已提交 + 写隔离(全局锁) |
| 性能 | 高(无锁) | 中等(有全局锁,TC 节点压力) |
| 侵入性 | 低(只需实现补偿接口) | 中(需接入 Seata 客户端) |
| 适用场景 | 长事务、跨异构系统 | 短事务、同技术栈微服务 |
| 补偿可靠性 | 业务方保证 | 框架自动重试 + 手动回滚 |
| 超时场景 | 协调器超时触发补偿 | 全局锁超时回滚,不阻塞 |
真实案例:某物流系统用 Seata AT 管理运单分发流程,但每单涉及 3 个外部物流商 API 调用(非关系型数据库),Seata 的自动 UNDO 回滚无法回滚 HTTP 请求。后来改纯 Saga,手工写 3 个补偿接口(调用物流商取消运单 API),流程反而更可控。
生产实战:Saga 协调器的异常处理策略
// 更完善的 Saga 协调器(含超时和重试)
@Component
public class RobustSagaOrchestrator {
private static final Duration STEP_TIMEOUT = Duration.ofSeconds(10);
private static final int MAX_RETRIES = 3;
private static final Duration COMPENSATE_TIMEOUT = Duration.ofSeconds(30);
public SagaResult execute(String sagaId, List<SagaStep> steps) {
stateRepo.save(sagaId, SagaStatus.STARTED);
// 正向执行
for (int i = 0; i < steps.size(); i++) {
SagaStep step = steps.get(i);
try {
// 带超时的远程调用
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> step.execute(sagaId)
);
future.get(STEP_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
stateRepo.recordStep(sagaId, i, StepStatus.SUCCESS);
} catch (TimeoutException e) {
// 超时场景:不确定远端是否成功,必须查远端状态
log.warn("步骤 {} 超时,查询远端状态: sagaId={}", i, sagaId);
boolean actuallySucceeded = step.queryRemoteStatus(sagaId);
if (actuallySucceeded) {
stateRepo.recordStep(sagaId, i, StepStatus.SUCCESS);
continue; // 实际成功了,继续下一个
}
// 确实没成功,走补偿
return compensate(sagaId, steps, i);
} catch (Exception e) {
return compensate(sagaId, steps, i);
}
}
stateRepo.updateStatus(sagaId, SagaStatus.COMPLETED);
return SagaResult.success(sagaId);
}
private SagaResult compensate(String sagaId, List<SagaStep> steps, int failedAt) {
stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATING);
// 从失败步骤的前一个开始补偿,逆序执行
for (int i = failedAt - 1; i >= 0; i--) {
SagaStep step = steps.get(i);
// 补偿操作最多重试 5 次,每次间隔 2 秒
retryWithBackoff(5, Duration.ofSeconds(2), () -> {
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> step.compensate(sagaId)
);
future.get(COMPENSATE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
});
stateRepo.recordStep(sagaId, i, StepStatus.COMPENSATED);
}
stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATED);
return SagaResult.failed(sagaId, "步骤 " + failedAt + " 失败,已完成补偿");
}
private void retryWithBackoff(int maxRetries, Duration baseDelay, Runnable task) {
for (int i = 0; i < maxRetries; i++) {
try {
task.run();
return; // 成功
} catch (Exception e) {
if (i == maxRetries - 1) {
// 补偿都失败了,必须告警人工介入
log.error("补偿失败{}次,告警人工介入", maxRetries, e);
alertService.sendAlert("SAGA_COMPENSATE_FAILED",
"补偿重试" + maxRetries + "次全部失败,请人工处理");
throw e;
}
// 指数退避:2s, 4s, 8s, 16s
long waitMs = baseDelay.toMillis() * (1L << i);
Thread.sleep(waitMs);
}
}
}
}这段代码的关键设计:
- 超时后不是直接补偿,而是先查远端状态(因为 TCP 超时不能代表远端失败)
- 补偿重试用指数退避,最大 5 次,每次间隔 2s 起步
- 补偿全部失败后告警人工介入,不走死循环
- 每个步骤的补偿状态持久化,重启可恢复
总结
Saga 模式的核心要点:
| 维度 | 要点 |
|---|---|
| 核心思想 | 大事务拆小 + 每个事务配补偿操作 |
| 幂等性 | 补偿和正向操作都必须幂等,用去重表保证 |
| 隔离性 | 用语义锁/乐观锁/冲正抵消处理中间状态 |
| 补偿悬挂 | 补偿前先查远端状态,防止错误撤销成功操作 |
| 重启恢复 | 协调器持久化 Saga 状态,重启扫描未完成实例 |
| 选型原则 | 简单场景 Choreography,复杂场景 Orchestration |
| 与 TCC 关系 | 不是替代品,实践中混合使用——库存用 TCC,支付用 Saga |
| 超时处理 | 先查远端状态再决定补偿,不盲目补偿 |
面试话术示例:"Saga 不是分布式事务的替代品,而是业务上接受中间状态的权衡。我们团队用 Orchestration 模式实现订单 Saga,协调器持久化每个步骤的状态到数据库,宕机后通过定时任务恢复未完成的 Saga。补偿操作有去重表保证幂等,库存用语义锁防止其他 Saga 读到中间状态,补偿前先查远端状态防止补偿悬挂。生产运行 1 年,Saga 失败率约 0.3%,恢复率 100%。"
生产避坑十条:
- 补偿操作的事务边界必须和正向操作一致——不要在补偿里写业务逻辑,只做"撤销动作"
- 如果补偿也需要调用多个下游,那补偿本身也需要 Saga 模式,形成嵌套补偿。遇到这种情况,说明你的服务拆分粒度有问题,应该合并强关联的服务
- 超时场景不要直接补偿,先查远端状态。TCP 超时不代表远端没收到请求,盲目补偿会导致重复扣款
- 补偿失败必须要走告警通道,别静默吞掉异常。线上 90% 的分布式事务数据不一致问题,都是因为补偿失败后没人知道
- Saga 协调器必须是单线程 + 持久化状态机,不要用多线程并发执行 Saga 步骤,否则状态管理会乱
- 补偿操作的日志必须打印完整的 sagaId 和操作前后数据快照,方便人工介入时定位
- Saga 里不要混用同步 RPC 和异步 MQ——同步调用超时后补偿触发,但异步消息可能已经发出,导致补偿和消息同时执行
- 用 MQ 做 Choreography 时,事件消息必须带
sagaId和stepSequence,接收方根据这两个字段做幂等和顺序判断 - 补偿操作不要依赖外部系统的实时状态——可以用"补偿+延迟重试"模式,等外部系统状态稳定后再补偿
- 不要试图用 Saga 实现 100% 的数据一致性,接受"最终一致 + 对账修复"的兜底策略。对账系统才是分布式事务的最终兜底
参考:Chris Richardson. Microservices Patterns; 阿里中间件团队. Seata AT 模式原理; 宋净超等. 云原生分布式事务原理与实践; Caitie McCaffrey. Distributed Sagas: A Protocol for Coordinating Microservices; 京东中间件团队. Saga 模式在京东履约系统的实践.