分布式架构中基于 Saga 模式的订单系统一致性设计与落地实践
在单体时代,订单创建、库存扣减、账户扣款、优惠券核销,往往可以放在一个本地事务里一把梭:要么全成,要么全回滚。
但一旦系统拆成订单服务、库存服务、支付服务、营销服务,这件事就变味了——跨服务、跨库、跨网络,ACID 不再便宜,2PC/XA 又常常太重,性能、可用性、研发复杂度都不讨好。
这也是 Saga 模式在订单系统里经常出现的原因:不强求瞬时强一致,而是在业务可接受的窗口内,通过正向操作 + 补偿操作,实现最终一致性。
这篇文章我会从订单场景出发,把设计思路、取舍、代码和排障方法串起来,尽量不是“概念定义大全”,而是像带你走一遍真实落地过程。
背景与问题
先看一个典型下单链路:
- 用户提交订单
- 订单服务创建订单草稿
- 库存服务冻结库存
- 支付服务预扣金额
- 营销服务核销优惠券
- 全部成功后,订单状态变为
CONFIRMED
听上去很顺,但只要任何一步失败,就会出现一致性问题:
- 订单创建成功,库存冻结失败
- 库存冻结成功,支付预扣失败
- 支付成功了,但订单服务回调超时
- 营销核销成功,但后续订单取消了
- 网络抖动导致同一请求执行两次,出现重复扣减
如果我们用一句话概括这个问题,那就是:
订单系统的一致性,不只是“数据能不能写进去”,而是“跨服务状态能不能走到可解释、可恢复的终态”。
为什么很多团队最后会选 Saga
因为订单链路往往有这些现实约束:
- 服务已经拆分,数据库独立
- 请求量高,对长事务敏感
- 下游系统异构,未必支持 XA
- 要求高可用,不能因为一个服务抖动就全链路锁死
- 业务上允许“短时间不一致,但最终要拉齐”
Saga 正好适合这类问题。
先讲结论:订单系统里 Saga 解决的到底是什么
Saga 不是让分布式事务“像本地事务一样强一致”,而是解决下面三件事:
- 把一个大事务拆成多个本地事务
- 给每个本地事务定义补偿动作
- 通过编排或协同,把流程推进到成功或补偿完成
例如:
- 创建订单 -> 补偿是取消订单
- 冻结库存 -> 补偿是释放库存
- 预扣支付 -> 补偿是退款/撤销预授权
- 核销优惠券 -> 补偿是返还优惠券
这里最关键的一点是:补偿不等于数据库回滚,而是新的业务操作。
核心原理
Saga 的两种实现方式
Saga 通常有两种落地形式:
-
Choreography(事件协同)
- 服务之间通过事件互相驱动
- 没有中央指挥者
- 耦合看起来低,但流程可视化和排障更难
-
Orchestration(中心编排)
- 由一个 Saga Orchestrator 统一调度各步骤
- 状态清晰,便于治理
- 需要设计编排器和状态持久化
对于订单系统,我更推荐 编排式 Saga。原因很现实:
- 订单流程通常长且复杂
- 失败路径多
- 运营、客服、财务都需要看流程状态
- 需要人工干预、重试、超时控制
订单 Saga 的核心状态机
下面是一个简化后的状态流转:
stateDiagram-v2
[*] --> PENDING
PENDING --> INVENTORY_RESERVED: 库存冻结成功
INVENTORY_RESERVED --> PAYMENT_HELD: 支付预扣成功
PAYMENT_HELD --> COUPON_APPLIED: 优惠券核销成功
COUPON_APPLIED --> CONFIRMED: 订单确认
INVENTORY_RESERVED --> CANCELLING: 支付失败
PAYMENT_HELD --> CANCELLING: 优惠券失败
PENDING --> FAILED: 订单创建失败
CANCELLING --> COMPENSATED: 库存释放/退款/返券完成
CANCELLING --> COMPENSATION_FAILED: 补偿失败待人工介入
一个完整执行序列
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant S as Saga编排器
participant I as 库存服务
participant P as 支付服务
participant C as 优惠券服务
U->>O: 提交订单
O->>S: 启动Saga
S->>O: 创建订单(PENDING)
O-->>S: 成功
S->>I: 冻结库存
I-->>S: 成功
S->>P: 预扣支付
P-->>S: 成功
S->>C: 核销优惠券
C-->>S: 失败
S->>P: 补偿-退款/撤销预扣
S->>I: 补偿-释放库存
S->>O: 补偿-取消订单
S-->>O: Saga结束(COMPENSATED)
本质设计点:顺序、幂等、可恢复
真正决定 Saga 好不好用的,不是有没有“流程图”,而是以下 3 个能力:
1. 顺序正确
常见执行顺序:
- 先创建订单
- 再冻结库存
- 再支付预扣
- 最后核销优惠券或确认权益
这样设计的原因是:
- 订单先落库,后续失败才有锚点可追踪
- 库存通常比支付更适合先冻结,避免超卖
- 支付建议“预扣/授权”而非直接不可逆扣款
- 越不可逆的动作越靠后
2. 幂等必须强制实现
因为网络超时、消息重复投递、编排器重试,都可能导致同一步骤执行多次。
所以每个服务接口都要支持:
sagaIdstepNamerequestId/ 幂等键
否则你会遇到这类事故:
- 库存冻结两次
- 优惠券返还两次
- 支付退款两次
我踩过一个坑:接口写了“重试”,却没写幂等表,结果高峰期一波超时重放直接把库存冻结翻倍,最后靠人工修数据,代价极高。
3. 可恢复比“一次成功”更重要
分布式系统里最常见的不是彻底失败,而是:
- 请求发出去了,但响应丢了
- 编排器挂了,步骤执行了一半
- 下游服务成功了,但状态没回写
- 补偿执行到一半卡住了
所以 Saga 的重点不是“每次都不出错”,而是:
出错后能自动重试、能从状态恢复、能人工接管。
方案对比与取舍分析
Saga vs TCC vs 2PC
| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| 2PC/XA | 强一致 | 较差 | 高 | 传统数据库、低并发、短链路 |
| TCC | 较强 | 中 | 很高 | 核心资金链路、强控制业务 |
| Saga | 最终一致 | 较好 | 中 | 订单、履约、营销等长流程 |
为什么订单系统往往更偏 Saga
订单链路里有很多操作天然适合补偿而不是回滚:
- 冻结库存 -> 释放库存
- 取消订单 -> 改状态
- 核销优惠券 -> 返券
- 支付预授权 -> 撤销授权
但如果你的场景是账户余额强一致扣减、清结算核心账务,那就要谨慎评估 Saga,可能 TCC 更合适。
边界条件
Saga 很适合这些前提:
- 业务允许短暂不一致
- 每个步骤都能定义补偿动作
- 不可逆动作可以后置
- 有完善的监控和人工处理机制
如果有步骤无法补偿,且业务又不接受中间态,那么 Saga 不是最佳方案。
架构设计:一个可落地的订单 Saga 方案
推荐组件划分
- Order Service
- 管理订单主状态
- Saga Orchestrator
- 持久化 Saga 状态
- 驱动步骤执行
- 超时扫描、失败重试、补偿调度
- Inventory Service
- 冻结/释放库存
- Payment Service
- 预扣/撤销/退款
- Coupon Service
- 核销/返还优惠券
- Outbox + Message Broker
- 保障本地事务与事件发布一致
关键表设计
至少建议有这几类表:
orderssaga_instancessaga_stepsidempotency_recordsoutbox_events
下面用类图快速表达一下关系:
classDiagram
class Order {
+string orderId
+string userId
+decimal amount
+string status
}
class SagaInstance {
+string sagaId
+string businessId
+string status
+datetime createdAt
+datetime updatedAt
}
class SagaStep {
+string sagaId
+string stepName
+string status
+int retryCount
+string compensationStatus
}
class IdempotencyRecord {
+string idemKey
+string serviceName
+string result
}
class OutboxEvent {
+string eventId
+string aggregateId
+string topic
+string payload
+string status
}
SagaInstance --> SagaStep
Order --> SagaInstance
实战代码(可运行)
下面给一个简化但可运行的 Python 示例,用编排式 Saga 模拟订单、库存、支付、优惠券四个步骤。
它不是生产级框架,但足够把核心机制说明白:状态推进、失败补偿、幂等控制。
运行环境:Python 3.10+
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Callable, Optional
import uuid
class StepStatus(str, Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
COMPENSATED = "COMPENSATED"
SKIPPED = "SKIPPED"
@dataclass
class StepRecord:
name: str
status: StepStatus = StepStatus.PENDING
error: Optional[str] = None
@dataclass
class SagaContext:
saga_id: str
order_id: str
user_id: str
amount: int
items: Dict[str, int]
coupon: Optional[str] = None
step_records: Dict[str, StepRecord] = field(default_factory=dict)
class IdempotencyStore:
def __init__(self):
self.done = set()
def execute_once(self, key: str, func: Callable):
if key in self.done:
print(f"[幂等] 已执行过: {key}")
return
func()
self.done.add(key)
class OrderService:
def __init__(self):
self.orders = {}
def create_order(self, ctx: SagaContext):
self.orders[ctx.order_id] = {
"user_id": ctx.user_id,
"amount": ctx.amount,
"status": "PENDING"
}
print(f"[Order] 创建订单成功: {ctx.order_id}")
def cancel_order(self, ctx: SagaContext):
if ctx.order_id in self.orders:
self.orders[ctx.order_id]["status"] = "CANCELLED"
print(f"[Order] 取消订单: {ctx.order_id}")
def confirm_order(self, ctx: SagaContext):
self.orders[ctx.order_id]["status"] = "CONFIRMED"
print(f"[Order] 确认订单: {ctx.order_id}")
class InventoryService:
def __init__(self):
self.stock = {"sku-1": 10, "sku-2": 5}
self.reserved = {}
def reserve(self, ctx: SagaContext):
for sku, qty in ctx.items.items():
if self.stock.get(sku, 0) < qty:
raise Exception(f"库存不足: {sku}")
for sku, qty in ctx.items.items():
self.stock[sku] -= qty
self.reserved[(ctx.order_id, sku)] = qty
print(f"[Inventory] 冻结库存成功: {ctx.items}")
def release(self, ctx: SagaContext):
for sku in list(ctx.items.keys()):
key = (ctx.order_id, sku)
qty = self.reserved.get(key, 0)
if qty > 0:
self.stock[sku] += qty
del self.reserved[key]
print(f"[Inventory] 释放库存完成: {ctx.order_id}")
class PaymentService:
def __init__(self):
self.held = {}
def hold(self, ctx: SagaContext):
if ctx.amount > 1000:
raise Exception("支付风控拒绝:金额超过限制")
self.held[ctx.order_id] = ctx.amount
print(f"[Payment] 预扣成功: {ctx.amount}")
def refund(self, ctx: SagaContext):
if ctx.order_id in self.held:
del self.held[ctx.order_id]
print(f"[Payment] 撤销预扣/退款完成: {ctx.order_id}")
class CouponService:
def __init__(self):
self.used = set()
def apply(self, ctx: SagaContext):
if not ctx.coupon:
print("[Coupon] 无优惠券,跳过")
return
if ctx.coupon == "BAD-COUPON":
raise Exception("优惠券不可用")
self.used.add(ctx.coupon)
print(f"[Coupon] 核销成功: {ctx.coupon}")
def restore(self, ctx: SagaContext):
if ctx.coupon and ctx.coupon in self.used:
self.used.remove(ctx.coupon)
print(f"[Coupon] 返还优惠券完成: {ctx.coupon}")
class SagaOrchestrator:
def __init__(self, order_svc, inventory_svc, payment_svc, coupon_svc, idem_store):
self.order_svc = order_svc
self.inventory_svc = inventory_svc
self.payment_svc = payment_svc
self.coupon_svc = coupon_svc
self.idem_store = idem_store
self.steps = [
("create_order", self._create_order, self._cancel_order),
("reserve_inventory", self._reserve_inventory, self._release_inventory),
("hold_payment", self._hold_payment, self._refund_payment),
("apply_coupon", self._apply_coupon, self._restore_coupon),
]
def run(self, ctx: SagaContext):
completed: List[str] = []
for step_name, action, compensation in self.steps:
ctx.step_records[step_name] = StepRecord(name=step_name)
try:
action(ctx)
ctx.step_records[step_name].status = StepStatus.SUCCESS
completed.append(step_name)
except Exception as e:
ctx.step_records[step_name].status = StepStatus.FAILED
ctx.step_records[step_name].error = str(e)
print(f"[Saga] 步骤失败: {step_name}, error={e}")
self.compensate(ctx, completed)
return False
self.order_svc.confirm_order(ctx)
print(f"[Saga] 全部步骤成功,订单完成: {ctx.order_id}")
return True
def compensate(self, ctx: SagaContext, completed_steps: List[str]):
print("[Saga] 开始补偿...")
compensation_map = {
"create_order": self._cancel_order,
"reserve_inventory": self._release_inventory,
"hold_payment": self._refund_payment,
"apply_coupon": self._restore_coupon,
}
for step_name in reversed(completed_steps):
try:
compensation_map[step_name](ctx)
ctx.step_records[step_name].status = StepStatus.COMPENSATED
except Exception as e:
print(f"[Saga] 补偿失败: {step_name}, error={e}")
def _create_order(self, ctx: SagaContext):
key = f"{ctx.saga_id}:create_order"
self.idem_store.execute_once(key, lambda: self.order_svc.create_order(ctx))
def _cancel_order(self, ctx: SagaContext):
key = f"{ctx.saga_id}:cancel_order"
self.idem_store.execute_once(key, lambda: self.order_svc.cancel_order(ctx))
def _reserve_inventory(self, ctx: SagaContext):
key = f"{ctx.saga_id}:reserve_inventory"
self.idem_store.execute_once(key, lambda: self.inventory_svc.reserve(ctx))
def _release_inventory(self, ctx: SagaContext):
key = f"{ctx.saga_id}:release_inventory"
self.idem_store.execute_once(key, lambda: self.inventory_svc.release(ctx))
def _hold_payment(self, ctx: SagaContext):
key = f"{ctx.saga_id}:hold_payment"
self.idem_store.execute_once(key, lambda: self.payment_svc.hold(ctx))
def _refund_payment(self, ctx: SagaContext):
key = f"{ctx.saga_id}:refund_payment"
self.idem_store.execute_once(key, lambda: self.payment_svc.refund(ctx))
def _apply_coupon(self, ctx: SagaContext):
key = f"{ctx.saga_id}:apply_coupon"
self.idem_store.execute_once(key, lambda: self.coupon_svc.apply(ctx))
def _restore_coupon(self, ctx: SagaContext):
key = f"{ctx.saga_id}:restore_coupon"
self.idem_store.execute_once(key, lambda: self.coupon_svc.restore(ctx))
if __name__ == "__main__":
order_svc = OrderService()
inventory_svc = InventoryService()
payment_svc = PaymentService()
coupon_svc = CouponService()
idem_store = IdempotencyStore()
orchestrator = SagaOrchestrator(
order_svc, inventory_svc, payment_svc, coupon_svc, idem_store
)
# 示例1:成功
ctx1 = SagaContext(
saga_id=str(uuid.uuid4()),
order_id="order-1001",
user_id="user-1",
amount=200,
items={"sku-1": 2},
coupon="CPN-OK"
)
print("\n=== 示例1:正常下单 ===")
orchestrator.run(ctx1)
# 示例2:失败并补偿
ctx2 = SagaContext(
saga_id=str(uuid.uuid4()),
order_id="order-1002",
user_id="user-2",
amount=200,
items={"sku-1": 1},
coupon="BAD-COUPON"
)
print("\n=== 示例2:优惠券失败触发补偿 ===")
orchestrator.run(ctx2)
print("\n=== 最终状态 ===")
print("订单:", order_svc.orders)
print("库存:", inventory_svc.stock)
print("支付冻结:", payment_svc.held)
print("已使用券:", coupon_svc.used)
运行后你会看到什么
- 示例1:订单顺利完成,状态会到
CONFIRMED - 示例2:优惠券核销失败,系统会执行:
- 撤销支付预扣
- 释放库存
- 取消订单
这就是 Saga 最核心的落地形态。
再进一步:生产环境怎么做得更稳
上面的代码是单进程演示,生产中通常要补齐下面几个关键能力。
1. Saga 状态持久化
不能只放内存。至少要持久化:
CREATE TABLE saga_instances (
saga_id VARCHAR(64) PRIMARY KEY,
business_id VARCHAR(64) NOT NULL,
saga_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE saga_steps (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
step_name VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
last_error TEXT,
updated_at TIMESTAMP NOT NULL,
UNIQUE KEY uk_saga_step (saga_id, step_name)
);
2. Outbox 模式保证“本地事务 + 发消息”一致
很多团队做 Saga 时,问题不是补偿逻辑,而是事件丢了。
例如:
- 订单库已经写成功
- 准备发“订单已创建”消息
- 结果 MQ 发送失败
这会导致下游永远收不到事件,Saga 卡死。
标准做法是 Outbox 模式:
- 订单写库
- 同一个本地事务里写入
outbox_events - 后台任务异步投递 MQ
- 投递成功后更新 outbox 状态
流程图如下:
flowchart TD
A[订单服务写订单] --> B[同事务写Outbox事件]
B --> C[提交本地事务]
C --> D[Outbox投递器扫描未发送事件]
D --> E[投递MQ]
E --> F[更新事件状态为SENT]
3. 超时扫描与自动恢复
必须有一个定时任务扫描:
- 长时间处于
PENDING的 Saga - 执行中但超过 SLA 的步骤
- 补偿失败的实例
处理方式可以是:
- 自动重试
- 查询下游最终状态后决定是否推进
- 转人工工单
常见坑与排查
这部分很重要,因为很多 Saga 方案不是死在设计图上,而是死在运行一周后的边缘场景里。
坑 1:补偿顺序错了
比如:
- 先返券
- 再退款
- 最后释放库存
看上去都补了,但业务上可能不合理。
通常应该按正向步骤的逆序补偿,因为后做的动作往往依赖前一步。
排查方法:
- 检查编排器是否严格按逆序执行 compensation
- 看补偿日志里 step 顺序
- 核对是否存在并发补偿
坑 2:把补偿当数据库回滚
Saga 补偿是新的业务动作,不会恢复到“时间倒流”的状态。
例如:
- 库存冻结后又释放,不等于“从来没冻结过”
- 用户可能已经收到通知短信
- 第三方支付可能已经留痕
建议:
- 所有补偿动作都做审计日志
- 对外通知单独设计撤销事件
- 业务侧接受“可解释中间态”
坑 3:幂等只做了正向,没做补偿
这是非常常见的事故源。
你可能已经给“冻结库存”做了幂等,却忘了“释放库存”也会被重试。
结果就是:
- 正向没重复
- 补偿重复执行,释放超量
排查方法:
- 分别检查 action 和 compensation 的幂等键
- 核对日志里同一
sagaId + step是否被多次执行 - 检查是否有基于业务主键的唯一约束
坑 4:下游成功了,但编排器认为失败
比如库存服务已经冻结成功,但响应超时,编排器把它当失败处理。
接下来如果又触发补偿,就会进入“成功但被误判”的经典分布式场景。
正确姿势:
- 不要只根据超时判定最终失败
- 提供步骤状态查询接口
- 重试前先查询下游是否已成功
例如:
SELECT status
FROM reservation_records
WHERE saga_id = 'xxx' AND step_name = 'reserve_inventory';
坑 5:订单状态设计过于粗糙
很多系统只有:
PENDINGSUCCESSFAILED
这在 Saga 场景里不够。你至少需要区分:
PENDINGPROCESSINGCONFIRMEDCANCELLINGCOMPENSATEDCOMPENSATION_FAILED
否则客服、运营、研发看同一个订单时,根本不知道它处于哪种恢复阶段。
一条推荐的排障路径
当你发现“订单卡住”时,建议按下面顺序查:
- 查
orders主状态 - 查
saga_instances当前状态 - 查
saga_steps哪一步失败 - 查该步骤请求日志、响应日志、重试记录
- 查下游服务是否实际执行成功
- 再决定是继续推进还是触发补偿
这个顺序很重要,不然很容易一上来就手工改订单状态,结果越修越乱。
安全/性能最佳实践
订单系统既要一致,也要扛量,下面这些建议很务实。
安全最佳实践
1. 补偿接口必须鉴权
补偿不是“内部接口就随便调”。
因为补偿动作往往意味着:
- 释放库存
- 退款
- 返还优惠券
- 取消订单
一旦被伪造调用,损失非常直接。
建议:
- 服务间使用 mTLS 或签名认证
- 补偿接口只允许内网/网关访问
- 对敏感动作记录操作者与来源
2. 防重放攻击
Saga 请求常带幂等键,但如果没有过期策略和签名校验,可能被恶意重放。
建议:
- 请求头加入时间戳和签名
- 幂等键设置有效期
- 核心资金类请求绑定业务流水号
3. 审计日志不可少
至少记录:
- sagaId
- orderId
- step
- action/compensation
- request payload 摘要
- result
- operator / system source
出了问题之后,审计日志就是“唯一真相来源”之一。
性能最佳实践
1. 不要把 Saga 编排器做成串行瓶颈
编排器可以统一控制流程,但不要所有实例只靠单线程处理。
建议:
- 按
orderId或sagaId分片 - 步骤执行异步化
- 定时扫描器分段并发执行
2. 缩短资源占用时间
库存冻结和支付预扣都有成本,别无限挂着。
建议:
- 给每个步骤设置超时时间
- 给订单设置支付/确认 TTL
- 超时后自动触发补偿
3. 热点资源要做隔离
大促时某些 SKU、某些券包会成为热点。
Saga 本身不解决热点竞争,还是要靠业务层控制。
建议:
- 库存分桶或预扣池
- 券码分段发放
- 对热点订单做限流与排队
容量估算与治理建议
架构落地时,不只是“能跑”,还得算清楚资源。
一个简单估算方法
假设:
- 峰值每秒 2000 单
- 每单平均 4 个 Saga 步骤
- 每步平均 1 次请求 + 0.1 次重试
- 失败补偿比例 2%
那么系统调用量大致是:
- 正向调用:
2000 * 4 = 8000 QPS - 重试附加:
8000 * 0.1 = 800 QPS - 补偿调用:
2000 * 2% * 平均3步补偿 ≈ 120 QPS
总量约 8920 QPS,这还不含:
- 状态查询
- MQ 投递
- 定时扫描
- 审计日志
所以做容量规划时,别只盯着“下单入口 QPS”,Saga 的控制面流量也要算进去。
治理上建议优先建设的能力
如果团队资源有限,我建议优先级如下:
- 幂等
- Saga 状态持久化
- 补偿能力
- 超时扫描
- 可观测性(日志/指标/链路追踪)
- 人工干预后台
因为现实里最怕的不是“自动化不够炫”,而是出问题后没人知道该怎么收场。
一套比较实用的落地清单
如果你准备在订单系统里上 Saga,可以按这份清单逐项核对:
- 每个步骤都有明确的正向动作与补偿动作
- 不可逆操作尽量后置
- 正向接口和补偿接口都支持幂等
- 有统一的
sagaId、orderId、requestId - Saga 实例和步骤状态已持久化
- 订单状态能表达处理中、补偿中、补偿失败
- 使用 Outbox 避免事件丢失
- 有超时扫描与自动恢复
- 有失败告警与人工接管入口
- 已进行重复投递、超时、半成功场景演练
总结
Saga 之所以适合订单系统,不是因为它“高级”,而是因为它尊重分布式系统的现实:
- 网络会抖
- 服务会超时
- 请求会重复
- 状态会分叉
- 人工介入有时不可避免
所以,设计订单一致性时,真正有效的思路不是执着于“像单机事务那样一把成功”,而是建立一套可推进、可补偿、可重试、可审计、可恢复的机制。
最后给 3 条可直接执行的建议:
-
先把状态机画清楚,再写代码
订单状态、Saga 状态、步骤状态分开设计,很多混乱会提前暴露。 -
先做幂等和补偿,再谈自动重试
没有幂等的重试,等于放大事故。 -
为失败而设计,而不是只为成功路径设计
生产环境里,失败路径往往比成功路径更决定系统质量。
如果你的订单链路已经拆成多个服务,且业务允许最终一致性,那么编排式 Saga 基本是一个值得认真投入的方向。
但也别神化它:它解决的是“如何把不一致收回来”,不是“让不一致永远不发生”。