跳转到内容
123xiao | 无名键客

《分布式架构中基于 Saga 模式的订单系统一致性设计与落地实践-389》

字数: 0 阅读时长: 1 分钟

分布式架构中基于 Saga 模式的订单系统一致性设计与落地实践

在单体时代,订单创建、库存扣减、账户扣款、优惠券核销,往往可以放在一个本地事务里一把梭:要么全成,要么全回滚。
但一旦系统拆成订单服务、库存服务、支付服务、营销服务,这件事就变味了——跨服务、跨库、跨网络,ACID 不再便宜,2PC/XA 又常常太重,性能、可用性、研发复杂度都不讨好。

这也是 Saga 模式在订单系统里经常出现的原因:不强求瞬时强一致,而是在业务可接受的窗口内,通过正向操作 + 补偿操作,实现最终一致性。

这篇文章我会从订单场景出发,把设计思路、取舍、代码和排障方法串起来,尽量不是“概念定义大全”,而是像带你走一遍真实落地过程。


背景与问题

先看一个典型下单链路:

  1. 用户提交订单
  2. 订单服务创建订单草稿
  3. 库存服务冻结库存
  4. 支付服务预扣金额
  5. 营销服务核销优惠券
  6. 全部成功后,订单状态变为 CONFIRMED

听上去很顺,但只要任何一步失败,就会出现一致性问题:

  • 订单创建成功,库存冻结失败
  • 库存冻结成功,支付预扣失败
  • 支付成功了,但订单服务回调超时
  • 营销核销成功,但后续订单取消了
  • 网络抖动导致同一请求执行两次,出现重复扣减

如果我们用一句话概括这个问题,那就是:

订单系统的一致性,不只是“数据能不能写进去”,而是“跨服务状态能不能走到可解释、可恢复的终态”。

为什么很多团队最后会选 Saga

因为订单链路往往有这些现实约束:

  • 服务已经拆分,数据库独立
  • 请求量高,对长事务敏感
  • 下游系统异构,未必支持 XA
  • 要求高可用,不能因为一个服务抖动就全链路锁死
  • 业务上允许“短时间不一致,但最终要拉齐”

Saga 正好适合这类问题。


先讲结论:订单系统里 Saga 解决的到底是什么

Saga 不是让分布式事务“像本地事务一样强一致”,而是解决下面三件事:

  1. 把一个大事务拆成多个本地事务
  2. 给每个本地事务定义补偿动作
  3. 通过编排或协同,把流程推进到成功或补偿完成

例如:

  • 创建订单 -> 补偿是取消订单
  • 冻结库存 -> 补偿是释放库存
  • 预扣支付 -> 补偿是退款/撤销预授权
  • 核销优惠券 -> 补偿是返还优惠券

这里最关键的一点是:补偿不等于数据库回滚,而是新的业务操作。


核心原理

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. 幂等必须强制实现

因为网络超时、消息重复投递、编排器重试,都可能导致同一步骤执行多次。
所以每个服务接口都要支持:

  • sagaId
  • stepName
  • requestId / 幂等键

否则你会遇到这类事故:

  • 库存冻结两次
  • 优惠券返还两次
  • 支付退款两次

我踩过一个坑:接口写了“重试”,却没写幂等表,结果高峰期一波超时重放直接把库存冻结翻倍,最后靠人工修数据,代价极高。

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
    • 保障本地事务与事件发布一致

关键表设计

至少建议有这几类表:

  1. orders
  2. saga_instances
  3. saga_steps
  4. idempotency_records
  5. outbox_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 模式

  1. 订单写库
  2. 同一个本地事务里写入 outbox_events
  3. 后台任务异步投递 MQ
  4. 投递成功后更新 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:订单状态设计过于粗糙

很多系统只有:

  • PENDING
  • SUCCESS
  • FAILED

这在 Saga 场景里不够。你至少需要区分:

  • PENDING
  • PROCESSING
  • CONFIRMED
  • CANCELLING
  • COMPENSATED
  • COMPENSATION_FAILED

否则客服、运营、研发看同一个订单时,根本不知道它处于哪种恢复阶段。


一条推荐的排障路径

当你发现“订单卡住”时,建议按下面顺序查:

  1. orders 主状态
  2. saga_instances 当前状态
  3. saga_steps 哪一步失败
  4. 查该步骤请求日志、响应日志、重试记录
  5. 查下游服务是否实际执行成功
  6. 再决定是继续推进还是触发补偿

这个顺序很重要,不然很容易一上来就手工改订单状态,结果越修越乱。


安全/性能最佳实践

订单系统既要一致,也要扛量,下面这些建议很务实。

安全最佳实践

1. 补偿接口必须鉴权

补偿不是“内部接口就随便调”。
因为补偿动作往往意味着:

  • 释放库存
  • 退款
  • 返还优惠券
  • 取消订单

一旦被伪造调用,损失非常直接。

建议:

  • 服务间使用 mTLS 或签名认证
  • 补偿接口只允许内网/网关访问
  • 对敏感动作记录操作者与来源

2. 防重放攻击

Saga 请求常带幂等键,但如果没有过期策略和签名校验,可能被恶意重放。

建议:

  • 请求头加入时间戳和签名
  • 幂等键设置有效期
  • 核心资金类请求绑定业务流水号

3. 审计日志不可少

至少记录:

  • sagaId
  • orderId
  • step
  • action/compensation
  • request payload 摘要
  • result
  • operator / system source

出了问题之后,审计日志就是“唯一真相来源”之一。


性能最佳实践

1. 不要把 Saga 编排器做成串行瓶颈

编排器可以统一控制流程,但不要所有实例只靠单线程处理。

建议:

  • orderIdsagaId 分片
  • 步骤执行异步化
  • 定时扫描器分段并发执行

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 的控制面流量也要算进去。

治理上建议优先建设的能力

如果团队资源有限,我建议优先级如下:

  1. 幂等
  2. Saga 状态持久化
  3. 补偿能力
  4. 超时扫描
  5. 可观测性(日志/指标/链路追踪)
  6. 人工干预后台

因为现实里最怕的不是“自动化不够炫”,而是出问题后没人知道该怎么收场。


一套比较实用的落地清单

如果你准备在订单系统里上 Saga,可以按这份清单逐项核对:

  • 每个步骤都有明确的正向动作与补偿动作
  • 不可逆操作尽量后置
  • 正向接口和补偿接口都支持幂等
  • 有统一的 sagaIdorderIdrequestId
  • Saga 实例和步骤状态已持久化
  • 订单状态能表达处理中、补偿中、补偿失败
  • 使用 Outbox 避免事件丢失
  • 有超时扫描与自动恢复
  • 有失败告警与人工接管入口
  • 已进行重复投递、超时、半成功场景演练

总结

Saga 之所以适合订单系统,不是因为它“高级”,而是因为它尊重分布式系统的现实:

  • 网络会抖
  • 服务会超时
  • 请求会重复
  • 状态会分叉
  • 人工介入有时不可避免

所以,设计订单一致性时,真正有效的思路不是执着于“像单机事务那样一把成功”,而是建立一套可推进、可补偿、可重试、可审计、可恢复的机制。

最后给 3 条可直接执行的建议:

  1. 先把状态机画清楚,再写代码
    订单状态、Saga 状态、步骤状态分开设计,很多混乱会提前暴露。

  2. 先做幂等和补偿,再谈自动重试
    没有幂等的重试,等于放大事故。

  3. 为失败而设计,而不是只为成功路径设计
    生产环境里,失败路径往往比成功路径更决定系统质量。

如果你的订单链路已经拆成多个服务,且业务允许最终一致性,那么编排式 Saga 基本是一个值得认真投入的方向。
但也别神化它:它解决的是“如何把不一致收回来”,不是“让不一致永远不发生”。


分享到:

上一篇
《从零到一参与开源项目:中级开发者的选型、提 Issue 与首次贡献实战指南》
下一篇
《微服务架构中的分布式事务实战:基于 Saga 模式的设计、落地与故障补偿》