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

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

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

背景与问题

订单系统几乎是分布式系统里最容易“看起来简单,做起来翻车”的场景之一。

一个典型下单流程,往往不只是“写一条订单记录”这么简单,而是会串起多个服务:

  • 订单服务:创建订单
  • 库存服务:冻结或扣减库存
  • 支付服务:创建支付单、完成扣款
  • 营销服务:锁定优惠券
  • 积分服务:发放或回滚积分
  • 物流服务:创建发货单

如果把这些服务放在单体应用里,用本地事务还能相对容易地保证一致性;但一旦拆成微服务,问题就立刻出现了:

  • 一个服务成功,另一个服务失败,怎么办?
  • 网络超时了,到底是没执行,还是执行成功但响应丢了?
  • 用户重复点击“提交订单”,如何避免重复扣库存、重复扣款?
  • 某个补偿动作本身又失败了,系统会不会卡死在半路?

很多团队最开始会直觉性地想用 分布式事务(2PC/XA)。但在互联网订单系统里,2PC 往往有几个现实问题:

  1. 性能差:事务协调过程长,资源锁持有时间长。
  2. 可用性差:某个参与者卡住,整体都可能被拖死。
  3. 工程复杂度高:跨语言、跨存储、跨中间件时很难统一落地。
  4. 不适合高并发链路:尤其是订单、支付这类核心业务,优先级通常是“可用 + 可恢复”,而不是“强一致且阻塞”。

所以,很多订单系统最后会走向一个更现实的方案:基于 Saga 模式实现最终一致性

这篇文章,我会从“订单全链路一致性设计”这个角度,把 Saga 模式怎么落到工程里讲透,包括:

  • Saga 到底解决什么问题
  • 编排式与协同式怎么选
  • 订单状态机如何设计
  • 代码怎么写才真的能跑
  • 补偿、幂等、超时、重试这些坑怎么避

先给结论:Saga 适合什么,不适合什么

先别急着上代码,先把边界讲清楚。

Saga 适合的场景

Saga 适用于这类业务:

  • 流程跨多个微服务
  • 每个步骤都可以独立提交本地事务
  • 某个步骤失败后,可以通过“补偿动作”进行语义回滚
  • 能接受短时间不一致
  • 更关注系统吞吐、可用性和恢复能力

比如订单系统中的:

  • 创建订单
  • 锁库存
  • 锁优惠券
  • 发起支付
  • 支付失败后释放资源

这些都很适合 Saga。

Saga 不适合的场景

Saga 并不是银弹。下面这些场景要谨慎:

  • 绝对强一致要求,例如核心账务总账、证券撮合
  • 某些操作不可逆,且无合理补偿语义
  • 补偿成本极高,甚至会触发更大业务风险
  • 多步骤之间需要长时间锁资源

比如“银行总账借贷平衡”就不太适合简单 Saga,通常需要更严格的账务模型与对账体系。


核心原理

Saga 的本质并不复杂:

把一个长事务拆成多个本地事务,每个本地事务独立提交;如果后续步骤失败,就按相反顺序执行补偿操作,最终达到业务一致。

一个订单 Saga 的基本步骤

以“创建订单并完成资源预留”为例:

  1. 创建订单(状态:PENDING)
  2. 冻结库存
  3. 锁定优惠券
  4. 创建支付单
  5. 全部成功后,订单状态更新为 CONFIRMED
  6. 任一步骤失败,则按已完成步骤逆序补偿:
    • 取消支付单
    • 释放优惠券
    • 解冻库存
    • 取消订单

顺序图:成功链路

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_logsaga_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

原因很直接:

  • 你不能只把本地数据库写成功,却没把“下一步任务”发出去
  • 也不能消息发了,但本地状态没落库
  • 不然恢复时你都不知道该从哪开始

推荐模式

  1. 本地事务内:
    • 更新业务表
    • 插入 outbox_event 表
  2. 提交事务
  3. 后台投递器扫描 outbox_event 表发消息
  4. 消费方按幂等键处理

一个简化的 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 秒
  • 大量订单一起进入补偿队列
  • 补偿流量瞬间打爆库存、券服务

所以要做什么

  1. 补偿限流
    • 不要无限制并发回滚
  2. 分级队列
    • 核心订单高优先级,非核心事件低优先级
  3. 退避重试
    • 例如 1s / 5s / 30s / 5min
  4. 死信与人工兜底
    • 重试上限后进入人工处理池
  5. 隔离主链路
    • 补偿线程池、连接池与主交易线程池隔离

这是我在实际项目里踩过的坑:主流程失败不可怕,补偿风暴才可怕。


常见坑与排查

下面这些问题,几乎每个做 Saga 的团队都会遇到。

1. 补偿成功了,但订单状态没改回来

现象

  • 库存已解冻
  • 优惠券已释放
  • 订单状态仍然是 CANCELLING

常见原因

  • 最后一步更新订单状态失败
  • 状态更新和补偿动作不在一个可靠恢复流程中
  • 补偿任务执行成功,但日志未落库

排查思路

  1. saga_instancesaga_log
  2. 查各子服务资源状态
  3. 对比订单状态与实际资源占用是否一致
  4. 看是否存在“补偿动作成功但主状态更新失败”的日志

建议

把“补偿完成后更新订单状态”为独立可重试步骤,不要依赖一次性完成。


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_FAILEDCOMPENSATION_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 模式在订单系统里的价值,不是让你“假装有分布式事务”,而是让你在现实约束下,构建一个:

  • 可用
  • 可恢复
  • 可追踪
  • 可扩展

的一致性方案。

如果要把本文压缩成几条最重要的落地建议,我会给这几条:

  1. 核心订单链路优先采用编排式 Saga

    • 状态集中,排障清晰,适合复杂流程
  2. 先设计状态机,再写补偿代码

    • 没有状态机的 Saga,最后一定会变成“靠日志猜现场”
  3. 每一步必须幂等、可查询、可重试

    • 这是处理超时、重复、网络抖动的基础设施
  4. 补偿不是异常分支,而是主设计的一部分

    • 需要表结构、任务系统、监控、告警一起配套
  5. 主流程和补偿流程要隔离

    • 否则线上故障时,补偿流量会反过来打垮主交易
  6. 给人工兜底留入口

    • 最终一致性不是“永远自动一致”,而是“系统尽量自动修复,少数问题可人工收敛”

最后给一个边界判断:

  • 如果你的业务必须瞬时强一致,Saga 不是首选;
  • 如果你的业务允许短暂不一致,但必须高可用、可恢复、可扩展,那么 Saga 往往是订单系统里非常务实的一条路。

真正的工程设计,通常不是选“最完美”的方案,而是选最适合你当前业务约束的方案。Saga 的价值,也正在这里。


分享到:

上一篇
《从源码到生产:基于开源项目 Nacos 的服务注册与配置中心实战优化指南》
下一篇
《区块链智能合约安全审计实战:从常见漏洞识别到自动化检测流程搭建》