背景与问题
订单系统几乎是分布式系统里最容易“看起来简单,做起来翻车”的场景之一。
一个典型下单流程,往往不只是“写一条订单记录”这么简单,而是会串起多个服务:
- 订单服务:创建订单
- 库存服务:冻结或扣减库存
- 支付服务:创建支付单、完成扣款
- 营销服务:锁定优惠券
- 积分服务:发放或回滚积分
- 物流服务:创建发货单
如果把这些服务放在单体应用里,用本地事务还能相对容易地保证一致性;但一旦拆成微服务,问题就立刻出现了:
- 一个服务成功,另一个服务失败,怎么办?
- 网络超时了,到底是没执行,还是执行成功但响应丢了?
- 用户重复点击“提交订单”,如何避免重复扣库存、重复扣款?
- 某个补偿动作本身又失败了,系统会不会卡死在半路?
很多团队最开始会直觉性地想用 分布式事务(2PC/XA)。但在互联网订单系统里,2PC 往往有几个现实问题:
- 性能差:事务协调过程长,资源锁持有时间长。
- 可用性差:某个参与者卡住,整体都可能被拖死。
- 工程复杂度高:跨语言、跨存储、跨中间件时很难统一落地。
- 不适合高并发链路:尤其是订单、支付这类核心业务,优先级通常是“可用 + 可恢复”,而不是“强一致且阻塞”。
所以,很多订单系统最后会走向一个更现实的方案:基于 Saga 模式实现最终一致性。
这篇文章,我会从“订单全链路一致性设计”这个角度,把 Saga 模式怎么落到工程里讲透,包括:
- Saga 到底解决什么问题
- 编排式与协同式怎么选
- 订单状态机如何设计
- 代码怎么写才真的能跑
- 补偿、幂等、超时、重试这些坑怎么避
先给结论:Saga 适合什么,不适合什么
先别急着上代码,先把边界讲清楚。
Saga 适合的场景
Saga 适用于这类业务:
- 流程跨多个微服务
- 每个步骤都可以独立提交本地事务
- 某个步骤失败后,可以通过“补偿动作”进行语义回滚
- 能接受短时间不一致
- 更关注系统吞吐、可用性和恢复能力
比如订单系统中的:
- 创建订单
- 锁库存
- 锁优惠券
- 发起支付
- 支付失败后释放资源
这些都很适合 Saga。
Saga 不适合的场景
Saga 并不是银弹。下面这些场景要谨慎:
- 绝对强一致要求,例如核心账务总账、证券撮合
- 某些操作不可逆,且无合理补偿语义
- 补偿成本极高,甚至会触发更大业务风险
- 多步骤之间需要长时间锁资源
比如“银行总账借贷平衡”就不太适合简单 Saga,通常需要更严格的账务模型与对账体系。
核心原理
Saga 的本质并不复杂:
把一个长事务拆成多个本地事务,每个本地事务独立提交;如果后续步骤失败,就按相反顺序执行补偿操作,最终达到业务一致。
一个订单 Saga 的基本步骤
以“创建订单并完成资源预留”为例:
- 创建订单(状态:PENDING)
- 冻结库存
- 锁定优惠券
- 创建支付单
- 全部成功后,订单状态更新为
CONFIRMED - 任一步骤失败,则按已完成步骤逆序补偿:
- 取消支付单
- 释放优惠券
- 解冻库存
- 取消订单
顺序图:成功链路
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant I as 库存服务
participant C as 优惠券服务
participant P as 支付服务
U->>O: 提交订单
O->>O: 创建订单(PENDING)
O->>I: 冻结库存
I-->>O: 成功
O->>C: 锁定优惠券
C-->>O: 成功
O->>P: 创建支付单
P-->>O: 成功
O->>O: 更新订单(CONFIRMED)
O-->>U: 下单成功
顺序图:失败与补偿链路
sequenceDiagram
participant O as 订单服务
participant I as 库存服务
participant C as 优惠券服务
participant P as 支付服务
O->>I: 冻结库存
I-->>O: 成功
O->>C: 锁定优惠券
C-->>O: 成功
O->>P: 创建支付单
P-->>O: 失败/超时
O->>C: 释放优惠券(补偿)
C-->>O: 成功
O->>I: 解冻库存(补偿)
I-->>O: 成功
O->>O: 更新订单(CANCELLED)
Saga 的两种落地方式:编排式 vs 协同式
这是设计里绕不过去的选择。
1. 编排式 Saga(Orchestration)
由一个Saga 协调者统一控制流程:
- 下一步调谁
- 失败后补偿谁
- 当前 Saga 执行到哪一步
- 重试与超时策略
优点
- 流程可见性强
- 状态集中管理,便于排查
- 对复杂订单链路更友好
- 更适合有多个分支、回滚策略复杂的场景
缺点
- 协调器会成为核心组件
- 设计不好容易变“上帝服务”
2. 协同式 Saga(Choreography)
服务之间通过事件总线自行联动:
- 订单已创建 -> 库存服务收到事件冻结库存
- 库存冻结成功 -> 发布事件给优惠券服务
- 优惠券成功 -> 再通知支付服务
优点
- 服务解耦更强
- 不需要中心协调器
缺点
- 流程分散,排障难
- 链路长时,状态难追踪
- 容易出现“谁该补偿谁”不清晰
我在订单系统里的建议
对于核心订单链路,我更推荐:
主流程用编排式 Saga,领域扩展动作用事件驱动。
也就是:
- 订单、库存、优惠券、支付这些强关联步骤,用编排式统一控制
- 发短信、埋点、用户画像、风控通知这类旁路能力,用异步事件处理
这样既保住了核心链路的可控性,又不把所有事情都塞进协调器。
订单一致性设计:不是只有“补偿”这么简单
Saga 落地时,真正关键的不是“有个回滚逻辑”,而是状态设计。
订单状态机建议
至少不要只用“已创建/已支付/已取消”这么粗的状态。建议拆分出中间态。
stateDiagram-v2
[*] --> PENDING
PENDING --> RESOURCES_RESERVED: 库存/券锁定成功
RESOURCES_RESERVED --> PAYING: 创建支付单
PAYING --> CONFIRMED: 支付成功或支付单创建完成
PENDING --> CANCELLING: 任一步骤失败
RESOURCES_RESERVED --> CANCELLING: 后续步骤失败
PAYING --> CANCELLING: 支付创建失败/超时关闭
CANCELLING --> CANCELLED: 补偿完成
CANCELLING --> CANCEL_FAILED: 补偿部分失败
为什么需要中间态
因为分布式系统里,“失败”不是一个瞬时动作,而是一个过程:
- 库存已冻结,但券还没锁
- 券已锁定,但支付服务超时,结果未知
- 补偿发起了,但库存解冻失败,需要重试
如果没有中间态,你就无法回答这些最基本的问题:
- 订单现在到底处于什么阶段?
- 是否允许用户重复提交?
- 是否可以发起人工修复?
- 是否应该继续重试补偿?
一个很重要的设计原则
订单状态,不等于 Saga 状态;但两者必须能关联。
建议单独维护一张 saga_log 或 saga_instance 表,记录:
- saga_id
- business_id(订单号)
- current_step
- status(RUNNING / SUCCESS / COMPENSATING / FAILED)
- retry_count
- last_error
- 更新时间
这样订单状态对业务可读,Saga 状态对技术可追踪。
方案对比与取舍分析
| 方案 | 一致性 | 性能 | 实现复杂度 | 可观测性 | 适用场景 |
|---|---|---|---|---|---|
| 本地事务 | 强一致 | 高 | 低 | 高 | 单体/单库 |
| 2PC/XA | 强一致 | 低 | 高 | 中 | 少量核心强一致场景 |
| TCC | 最终一致 | 中 | 很高 | 中 | 高价值、可预留资源场景 |
| Saga | 最终一致 | 高 | 中 | 高(编排式) | 订单、履约、营销链路 |
| 纯事件最终一致 | 最终一致 | 高 | 中 | 低~中 | 弱流程约束场景 |
为什么订单常选 Saga 而不是 TCC
TCC 很强,但实现门槛也高:
- 每个服务都要提供 Try / Confirm / Cancel
- 资源预留语义要求很明确
- 对已有系统改造大
而订单域里,很多服务原本就有“创建/取消、冻结/解冻”能力,天然适合 Saga 的“正向操作 + 补偿操作”模型。
一句话总结:
TCC 更像“先预占资源再确认”,Saga 更像“做了再补偿”。
订单系统里,大多数团队会发现 Saga 的改造成本更可控。
实战代码(可运行)
下面给一个可运行的简化版编排式 Saga 示例,用 Python 模拟:
- 创建订单
- 冻结库存
- 锁定优惠券
- 创建支付单
- 失败后逆序补偿
这段代码不是生产级框架,但足够把核心设计讲清楚。
示例说明
- 用内存字典模拟数据库
- 每个服务有本地事务方法和补偿方法
- 协调器记录已成功步骤
- 某一步失败后自动补偿
- 加了基础幂等处理
可运行代码
from dataclasses import dataclass, field
from typing import Dict, List, Callable
import uuid
# ===== 模拟数据库 =====
db = {
"orders": {},
"inventory_frozen": {},
"coupon_locked": {},
"payments": {},
"saga_logs": {}
}
# ===== 领域异常 =====
class SagaStepError(Exception):
pass
# ===== 数据模型 =====
@dataclass
class SagaContext:
saga_id: str
order_id: str
user_id: str
sku_id: str
quantity: int
coupon_id: str
amount: int
completed_steps: List[str] = field(default_factory=list)
# ===== 服务实现 =====
class OrderService:
@staticmethod
def create_order(ctx: SagaContext):
order = db["orders"].get(ctx.order_id)
if order:
# 幂等:已创建则直接返回
return
db["orders"][ctx.order_id] = {
"order_id": ctx.order_id,
"user_id": ctx.user_id,
"sku_id": ctx.sku_id,
"quantity": ctx.quantity,
"amount": ctx.amount,
"status": "PENDING"
}
print(f"[OrderService] create_order success: {ctx.order_id}")
@staticmethod
def cancel_order(ctx: SagaContext):
order = db["orders"].get(ctx.order_id)
if not order:
return
if order["status"] == "CANCELLED":
return
order["status"] = "CANCELLED"
print(f"[OrderService] cancel_order success: {ctx.order_id}")
@staticmethod
def confirm_order(ctx: SagaContext):
order = db["orders"].get(ctx.order_id)
if not order:
raise SagaStepError("order not found")
if order["status"] == "CONFIRMED":
return
order["status"] = "CONFIRMED"
print(f"[OrderService] confirm_order success: {ctx.order_id}")
class InventoryService:
stock: Dict[str, int] = {"sku-1": 10}
@staticmethod
def freeze_stock(ctx: SagaContext):
if db["inventory_frozen"].get(ctx.order_id):
return
available = InventoryService.stock.get(ctx.sku_id, 0)
if available < ctx.quantity:
raise SagaStepError("insufficient stock")
InventoryService.stock[ctx.sku_id] -= ctx.quantity
db["inventory_frozen"][ctx.order_id] = {
"sku_id": ctx.sku_id,
"quantity": ctx.quantity,
"status": "FROZEN"
}
print(f"[InventoryService] freeze_stock success: {ctx.order_id}")
@staticmethod
def unfreeze_stock(ctx: SagaContext):
frozen = db["inventory_frozen"].get(ctx.order_id)
if not frozen:
return
if frozen["status"] == "RELEASED":
return
InventoryService.stock[frozen["sku_id"]] += frozen["quantity"]
frozen["status"] = "RELEASED"
print(f"[InventoryService] unfreeze_stock success: {ctx.order_id}")
class CouponService:
@staticmethod
def lock_coupon(ctx: SagaContext):
if db["coupon_locked"].get(ctx.order_id):
return
db["coupon_locked"][ctx.order_id] = {
"coupon_id": ctx.coupon_id,
"status": "LOCKED"
}
print(f"[CouponService] lock_coupon success: {ctx.order_id}")
@staticmethod
def release_coupon(ctx: SagaContext):
locked = db["coupon_locked"].get(ctx.order_id)
if not locked:
return
if locked["status"] == "RELEASED":
return
locked["status"] = "RELEASED"
print(f"[CouponService] release_coupon success: {ctx.order_id}")
class PaymentService:
FAIL_ON_CREATE = True # 用于模拟失败
@staticmethod
def create_payment(ctx: SagaContext):
if db["payments"].get(ctx.order_id):
return
if PaymentService.FAIL_ON_CREATE:
raise SagaStepError("payment service timeout")
db["payments"][ctx.order_id] = {
"order_id": ctx.order_id,
"amount": ctx.amount,
"status": "CREATED"
}
print(f"[PaymentService] create_payment success: {ctx.order_id}")
@staticmethod
def cancel_payment(ctx: SagaContext):
payment = db["payments"].get(ctx.order_id)
if not payment:
return
if payment["status"] == "CANCELLED":
return
payment["status"] = "CANCELLED"
print(f"[PaymentService] cancel_payment success: {ctx.order_id}")
# ===== Saga 协调器 =====
class SagaOrchestrator:
def __init__(self):
self.steps = [
("create_order", OrderService.create_order, OrderService.cancel_order),
("freeze_stock", InventoryService.freeze_stock, InventoryService.unfreeze_stock),
("lock_coupon", CouponService.lock_coupon, CouponService.release_coupon),
("create_payment", PaymentService.create_payment, PaymentService.cancel_payment),
]
def log(self, ctx: SagaContext, status: str, current_step: str = "", error: str = ""):
db["saga_logs"][ctx.saga_id] = {
"saga_id": ctx.saga_id,
"order_id": ctx.order_id,
"status": status,
"current_step": current_step,
"completed_steps": list(ctx.completed_steps),
"error": error
}
def execute(self, ctx: SagaContext):
self.log(ctx, status="RUNNING")
try:
for step_name, action, _ in self.steps:
self.log(ctx, status="RUNNING", current_step=step_name)
action(ctx)
ctx.completed_steps.append(step_name)
OrderService.confirm_order(ctx)
self.log(ctx, status="SUCCESS")
print(f"[Saga] success: {ctx.saga_id}")
except Exception as e:
print(f"[Saga] failed: {e}")
self.log(ctx, status="COMPENSATING", error=str(e))
self.compensate(ctx)
self.log(ctx, status="FAILED", error=str(e))
def compensate(self, ctx: SagaContext):
compensation_map = {
"create_order": OrderService.cancel_order,
"freeze_stock": InventoryService.unfreeze_stock,
"lock_coupon": CouponService.release_coupon,
"create_payment": PaymentService.cancel_payment,
}
for step_name in reversed(ctx.completed_steps):
try:
compensation = compensation_map[step_name]
compensation(ctx)
except Exception as e:
print(f"[Saga] compensation failed for step={step_name}, error={e}")
if __name__ == "__main__":
ctx = SagaContext(
saga_id=str(uuid.uuid4()),
order_id="order-1001",
user_id="user-1",
sku_id="sku-1",
quantity=2,
coupon_id="coupon-9",
amount=100
)
orchestrator = SagaOrchestrator()
orchestrator.execute(ctx)
print("\n==== final db state ====")
for k, v in db.items():
print(k, "=>", v)
如何运行
python saga_order_demo.py
因为代码里 PaymentService.FAIL_ON_CREATE = True,所以会模拟支付单创建失败,最终触发补偿。
如果你把它改成 False,则会走成功路径。
这段代码里体现了哪些落地点
虽然是简化版,但已经覆盖几个关键点:
- 本地事务拆分:每个服务单独提交
- 补偿逻辑显式定义:不是数据库回滚,而是业务回滚
- 逆序补偿:按已成功步骤反向恢复
- 幂等处理:重复执行时不产生副作用
- Saga 日志:保留当前进度,便于恢复与排查
生产落地时,建议补上 Outbox 和消息驱动恢复
上面的示例为了易懂,使用的是同步调用。但真实系统中,建议结合 事务消息 / Outbox Pattern。
原因很直接:
- 你不能只把本地数据库写成功,却没把“下一步任务”发出去
- 也不能消息发了,但本地状态没落库
- 不然恢复时你都不知道该从哪开始
推荐模式
- 本地事务内:
- 更新业务表
- 插入 outbox_event 表
- 提交事务
- 后台投递器扫描 outbox_event 表发消息
- 消费方按幂等键处理
一个简化的 Outbox 表结构
CREATE TABLE outbox_event (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'NEW',
retry_count INT NOT NULL DEFAULT 0,
next_retry_time DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_event_id (event_id),
KEY idx_status_retry (status, next_retry_time)
);
容量估算:别让补偿系统成为雪崩放大器
很多文章只讲模式,不讲容量,这在架构落地里是不够的。
一个简单估算方法
假设:
- 峰值每秒下单:2000 TPS
- 平均每个订单触发 4 个 Saga 步骤
- 平均失败率:2%
- 每个失败订单平均需要 3 次补偿调用
- 每次调用超时阈值:300ms
那么:
- 正常步骤调用量:
2000 * 4 = 8000 次/秒 - 失败订单数:
2000 * 2% = 40 单/秒 - 补偿调用量:
40 * 3 = 120 次/秒
看起来不大,但别忘了异常通常不是均匀发生,而是抖动式爆发:
- 支付服务超时 30 秒
- 大量订单一起进入补偿队列
- 补偿流量瞬间打爆库存、券服务
所以要做什么
- 补偿限流
- 不要无限制并发回滚
- 分级队列
- 核心订单高优先级,非核心事件低优先级
- 退避重试
- 例如 1s / 5s / 30s / 5min
- 死信与人工兜底
- 重试上限后进入人工处理池
- 隔离主链路
- 补偿线程池、连接池与主交易线程池隔离
这是我在实际项目里踩过的坑:主流程失败不可怕,补偿风暴才可怕。
常见坑与排查
下面这些问题,几乎每个做 Saga 的团队都会遇到。
1. 补偿成功了,但订单状态没改回来
现象
- 库存已解冻
- 优惠券已释放
- 订单状态仍然是
CANCELLING
常见原因
- 最后一步更新订单状态失败
- 状态更新和补偿动作不在一个可靠恢复流程中
- 补偿任务执行成功,但日志未落库
排查思路
- 查
saga_instance或saga_log - 查各子服务资源状态
- 对比订单状态与实际资源占用是否一致
- 看是否存在“补偿动作成功但主状态更新失败”的日志
建议
把“补偿完成后更新订单状态”为独立可重试步骤,不要依赖一次性完成。
2. 调用超时后,无法判断是否成功
现象
订单服务调库存服务超时,库存到底冻没冻,不知道。
这是分布式系统最经典的问题
超时 != 失败。
可能是:
- 请求根本没到
- 请求到了,执行失败
- 请求到了,执行成功,但响应丢了
解决方式
- 所有步骤必须支持幂等查询
- 使用
request_id / saga_id / business_key作为唯一幂等键 - 超时后先查状态,再决定重试还是补偿
推荐接口设计
{
"requestId": "saga-123-step-freeze-stock",
"orderId": "order-1001",
"skuId": "sku-1",
"quantity": 2
}
库存服务内部保存 requestId,如果重复请求到来,直接返回上次结果。
3. 补偿动作又失败了
现象
支付创建失败后,开始释放优惠券;结果优惠券服务也超时了。
正确认知
Saga 不是“失败马上恢复”,而是“失败后可恢复”。
建议策略
- 补偿动作必须支持重试
- 补偿状态必须持久化
- 允许进入
CANCEL_FAILED或COMPENSATION_PENDING中间态 - 提供人工修复台账
不要追求“一次补偿全成功”,重点是系统知道自己没补偿完。
4. 重复下单导致重复执行 Saga
现象
用户连点两次提交,或前端重试,导致两个相同订单并发创建。
解决方式
- 下单入口加防重令牌
- 订单维度增加业务唯一键
- Saga 实例表对
business_id + saga_type建唯一索引
例如:
CREATE TABLE saga_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
saga_type VARCHAR(64) NOT NULL,
business_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
current_step VARCHAR(64),
retry_count INT NOT NULL DEFAULT 0,
last_error TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_saga_id (saga_id),
UNIQUE KEY uk_business (saga_type, business_id)
);
5. 事件乱序导致状态回退
现象
先收到“支付成功”,后收到“支付创建中”,结果订单状态被覆盖回旧状态。
解决方式
- 每个事件带版本号或状态序号
- 只允许状态单向前进
- 更新时校验版本
例如状态序号:
- PENDING = 10
- RESOURCES_RESERVED = 20
- PAYING = 30
- CONFIRMED = 40
- CANCELLING = 50
- CANCELLED = 60
更新时只允许从合法前序状态迁移,不接受“倒退”。
安全/性能最佳实践
这一部分经常被忽略,但在线上特别关键。
安全实践
1. 幂等键不要可预测
如果你的 request_id 直接暴露业务规律,可能被恶意重放。建议:
- 使用 UUID / 雪花 ID
- 接口增加签名与过期时间
- 内部服务间走可信网络与鉴权
2. 补偿接口也要鉴权
别因为“这是内部接口”就忽略权限控制。补偿接口一旦被误调用,后果往往比正向接口更严重。
建议:
- mTLS 或服务间签名
- 细粒度 RBAC
- 审计日志记录调用来源
3. 敏感信息不要写进 Saga 日志
支付上下文里常见敏感字段:
- 用户身份证
- 手机号
- 支付账户标识
- 风控参数
Saga 日志只记必要信息,敏感字段要脱敏或引用化。
性能实践
1. 主链路尽量短
不要把所有非核心动作都塞进 Saga 主流程,比如:
- 发短信
- 用户成长值统计
- 推荐系统画像更新
这些都应该异步化。
2. 避免长事务式编排
协调器不要持有数据库事务贯穿整个链路。正确做法是:
- 每一步本地提交
- 协调器只记录流程状态
- 通过日志和消息驱动下一步
3. 步骤超时要分层设置
不同服务的超时不能一刀切:
- 库存服务:50~150ms
- 优惠券服务:100~200ms
- 支付创建:200~500ms
超时太短会误判失败,太长会拖垮线程池。
4. 补偿优先级低于正向交易
线上高峰期时,建议:
- 正向下单线程池独立
- 补偿线程池独立
- 补偿可延迟、可降速
- 核心交易优先保活
5. 每个步骤都要可观测
至少打通这些指标:
- Saga 成功率
- 每个步骤耗时 P95/P99
- 超时率
- 补偿触发率
- 补偿成功率
- 人工介入单量
如果没有这些指标,线上出问题时你基本只能靠猜。
一个更贴近生产的落地清单
如果你准备在订单系统里正式上 Saga,我建议按下面清单逐项核对。
设计层
- 明确每个步骤的正向动作和补偿动作
- 确认补偿是“业务可接受回滚”,不是技术幻觉
- 列出不可逆步骤,并给出兜底方案
- 定义订单状态机与 Saga 状态机
数据层
- Saga 实例表
- Saga 步骤日志表
- 幂等记录表
- Outbox 事件表
- 死信任务表
接口层
- 每个步骤支持幂等
- 每个步骤支持结果查询
- 补偿接口支持重复调用
- 超时后支持“查后再决定”
运维层
- 重试策略可配置
- 补偿任务可人工重放
- 异常 Saga 可检索
- 告警覆盖超时、补偿失败、积压
总结
Saga 模式在订单系统里的价值,不是让你“假装有分布式事务”,而是让你在现实约束下,构建一个:
- 可用
- 可恢复
- 可追踪
- 可扩展
的一致性方案。
如果要把本文压缩成几条最重要的落地建议,我会给这几条:
-
核心订单链路优先采用编排式 Saga
- 状态集中,排障清晰,适合复杂流程
-
先设计状态机,再写补偿代码
- 没有状态机的 Saga,最后一定会变成“靠日志猜现场”
-
每一步必须幂等、可查询、可重试
- 这是处理超时、重复、网络抖动的基础设施
-
补偿不是异常分支,而是主设计的一部分
- 需要表结构、任务系统、监控、告警一起配套
-
主流程和补偿流程要隔离
- 否则线上故障时,补偿流量会反过来打垮主交易
-
给人工兜底留入口
- 最终一致性不是“永远自动一致”,而是“系统尽量自动修复,少数问题可人工收敛”
最后给一个边界判断:
- 如果你的业务必须瞬时强一致,Saga 不是首选;
- 如果你的业务允许短暂不一致,但必须高可用、可恢复、可扩展,那么 Saga 往往是订单系统里非常务实的一条路。
真正的工程设计,通常不是选“最完美”的方案,而是选最适合你当前业务约束的方案。Saga 的价值,也正在这里。