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

《微服务架构下分布式事务实战:基于 Saga 模式的订单系统一致性设计与落地》

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

微服务架构下分布式事务实战:基于 Saga 模式的订单系统一致性设计与落地

在单体时代,订单创建、库存扣减、支付处理、优惠券核销,往往都在一个本地事务里完成。BEGIN 一开,出错就 ROLLBACK,简单直接。
但一旦系统拆成微服务,这套办法就失灵了:订单服务有自己的库,库存服务有自己的库,支付服务又是另一套系统,传统 ACID 很难跨服务、跨数据库、跨消息中间件继续生效。

我在做订单系统时,最常见的问题不是“事务怎么提交”,而是“半成功怎么办”:

  • 订单已经创建了,但库存没扣成功
  • 库存扣了,支付超时了
  • 支付成功了,订单状态却没更新
  • 补偿执行了两次,把库存加多了

这篇文章就围绕一个真实的订单场景,讲清楚 为什么 Saga 是微服务里更实际的分布式事务方案,以及 如何把它落到代码和工程细节中


背景与问题

先看一个典型下单链路:

  1. 用户提交订单
  2. 订单服务创建订单
  3. 库存服务冻结库存
  4. 支付服务扣款
  5. 订单服务将订单标记为已支付
  6. 营销服务核销优惠券

这些步骤分散在多个服务中,每个服务都有本地事务,但整个业务过程需要“最终一致”。

为什么不直接上 2PC?

理论上,两阶段提交(2PC)能保证强一致,但在微服务场景里常常不划算:

  • 参与方必须支持 XA,改造成本高
  • 协调器成为关键点,复杂度高
  • 长事务会锁资源,吞吐量差
  • 跨网络、跨异构存储时可用性下降

对于订单系统这类高并发业务,很多团队最后会选择:

  • 本地事务 + 可靠消息
  • TCC
  • Saga

而 Saga 的优势在于:

  • 对已有服务侵入相对可控
  • 不要求底层数据库支持分布式事务协议
  • 适合长流程业务
  • 通过补偿机制实现最终一致

当然,它不是银弹。Saga 牺牲的是强一致的即时性,换来更高的可用性和更现实的工程落地性。


方案对比与取舍分析

先把几个常见方案摆在一起看。

方案一致性实现难度业务侵入性能适用场景
2PC/XA强一致较差少量核心强一致场景
TCC强一致倾向很高很高资金、额度等强控制业务
Saga最终一致较好订单、履约、营销链路
本地消息表 + 异步重试最终一致事件驱动型流程

为什么订单系统更适合 Saga?

因为订单流程天然具备“阶段性”和“可补偿性”:

  • 创建订单失败:直接终止
  • 冻结库存失败:取消订单
  • 支付失败:解冻库存、关闭订单
  • 优惠券核销失败:返还优惠券、必要时回滚订单状态

换句话说,订单不是“非黑即白”的一次性提交,而是一个可以拆成多个局部动作的业务编排过程。


核心原理

Saga 的核心思想很简单:

把一个长事务拆成多个本地事务,每一步成功后继续下一步;如果中途失败,就按相反顺序执行补偿操作。

两种实现思路

1. Choreography(事件编排)

各个服务通过事件自行驱动:

  • 订单创建后发事件
  • 库存服务订阅并冻结库存,再发事件
  • 支付服务订阅并扣款,再发事件

优点是解耦,缺点是链路复杂后很难排查,容易形成“事件迷宫”。

2. Orchestration(集中式编排)

由一个 Saga 协调器统一控制流程:

  • 先调订单服务
  • 再调库存服务
  • 再调支付服务
  • 某步失败时触发补偿

对于中级工程师落地订单场景,我更建议先从 Orchestration 开始,因为:

  • 状态流转清楚
  • 便于审计与排查
  • 补偿路径统一
  • 更适合做超时控制和重试管理

订单 Saga 流程设计

下面以“下单 + 库存冻结 + 支付扣款”为例。

正向流程

  1. 创建订单,状态为 PENDING
  2. 冻结库存
  3. 发起支付
  4. 成功后订单改为 CONFIRMED

失败补偿流程

  • 如果库存冻结失败:取消订单
  • 如果支付失败:解冻库存 + 取消订单
  • 如果订单确认失败:需要重试确认,不能轻易回滚支付

这里有个非常重要的工程判断:

并不是每一步失败都应该立刻全量回滚。

例如支付成功但订单状态更新失败,这种情况更合理的处理是:

  • 把事件持久化
  • 重试订单确认
  • 人工兜底补偿

因为“支付退款”通常比“重试订单状态更新”成本更高、风险更大。


Mermaid:Saga 订单编排流程图

flowchart TD
    A[用户提交订单] --> B[订单服务: 创建订单 PENDING]
    B --> C[库存服务: 冻结库存]
    C --> D[支付服务: 扣款]
    D --> E[订单服务: 更新为 CONFIRMED]

    C -- 失败 --> C1[补偿: 取消订单]
    D -- 失败 --> D1[补偿: 解冻库存]
    D1 --> D2[补偿: 取消订单]
    E -- 失败 --> E1[记录异常事件并重试确认]

Mermaid:时序图

sequenceDiagram
    participant U as 用户
    participant O as Saga协调器
    participant OS as 订单服务
    participant IS as 库存服务
    participant PS as 支付服务

    U->>O: 提交下单请求
    O->>OS: createOrder()
    OS-->>O: orderId

    O->>IS: reserveStock(orderId)
    IS-->>O: success/fail

    alt 库存成功
        O->>PS: pay(orderId)
        PS-->>O: success/fail

        alt 支付成功
            O->>OS: confirmOrder(orderId)
            OS-->>O: success
        else 支付失败
            O->>IS: releaseStock(orderId)
            O->>OS: cancelOrder(orderId)
        end
    else 库存失败
        O->>OS: cancelOrder(orderId)
    end

数据模型设计

Saga 要落地,不能只停留在“画流程图”。你至少需要一张 Saga 状态表,来支持:

  • 当前执行到了哪一步
  • 每一步的执行结果
  • 是否需要重试
  • 是否已经补偿
  • 是否出现人工介入异常

Saga 状态表

CREATE TABLE saga_instance (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  saga_id VARCHAR(64) NOT NULL UNIQUE,
  order_id VARCHAR(64) NOT NULL,
  state VARCHAR(32) NOT NULL,
  current_step VARCHAR(32) NOT NULL,
  retry_count INT NOT NULL DEFAULT 0,
  last_error TEXT,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

订单表

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id VARCHAR(64) NOT NULL UNIQUE,
  user_id VARCHAR(64) NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

库存冻结记录表

这个表非常关键,用于保证冻结/解冻幂等。

CREATE TABLE stock_reservation (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id VARCHAR(64) NOT NULL UNIQUE,
  product_id VARCHAR(64) NOT NULL,
  quantity INT NOT NULL,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

实战代码(可运行)

下面我用 Python 做一个简化版可运行示例,模拟 Saga 协调器如何管理订单、库存、支付和补偿。
它不是生产级框架,但足够帮你把核心机制跑通。

运行环境:Python 3.10+

import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict


class OrderStatus(str, Enum):
    PENDING = "PENDING"
    CONFIRMED = "CONFIRMED"
    CANCELLED = "CANCELLED"


class ReservationStatus(str, Enum):
    RESERVED = "RESERVED"
    RELEASED = "RELEASED"


@dataclass
class Order:
    order_id: str
    user_id: str
    amount: float
    status: OrderStatus = OrderStatus.PENDING


@dataclass
class StockReservation:
    order_id: str
    product_id: str
    quantity: int
    status: ReservationStatus = ReservationStatus.RESERVED


class OrderService:
    def __init__(self):
        self.orders: Dict[str, Order] = {}

    def create_order(self, user_id: str, amount: float) -> str:
        order_id = str(uuid.uuid4())
        self.orders[order_id] = Order(order_id=order_id, user_id=user_id, amount=amount)
        print(f"[OrderService] 创建订单成功: {order_id}")
        return order_id

    def confirm_order(self, order_id: str):
        order = self.orders[order_id]
        if order.status == OrderStatus.CONFIRMED:
            print(f"[OrderService] 订单已确认,幂等返回: {order_id}")
            return
        if order.status == OrderStatus.CANCELLED:
            raise Exception("订单已取消,不能确认")
        order.status = OrderStatus.CONFIRMED
        print(f"[OrderService] 订单确认成功: {order_id}")

    def cancel_order(self, order_id: str):
        order = self.orders[order_id]
        if order.status == OrderStatus.CANCELLED:
            print(f"[OrderService] 订单已取消,幂等返回: {order_id}")
            return
        if order.status == OrderStatus.CONFIRMED:
            raise Exception("订单已确认,不能取消")
        order.status = OrderStatus.CANCELLED
        print(f"[OrderService] 订单取消成功: {order_id}")


class InventoryService:
    def __init__(self, initial_stock: int):
        self.available_stock = initial_stock
        self.reservations: Dict[str, StockReservation] = {}

    def reserve_stock(self, order_id: str, product_id: str, quantity: int):
        if order_id in self.reservations:
            reservation = self.reservations[order_id]
            if reservation.status == ReservationStatus.RESERVED:
                print(f"[InventoryService] 库存已冻结,幂等返回: {order_id}")
                return

        if self.available_stock < quantity:
            raise Exception("库存不足")

        self.available_stock -= quantity
        self.reservations[order_id] = StockReservation(
            order_id=order_id,
            product_id=product_id,
            quantity=quantity,
            status=ReservationStatus.RESERVED
        )
        print(f"[InventoryService] 冻结库存成功: {order_id}, 剩余库存={self.available_stock}")

    def release_stock(self, order_id: str):
        reservation = self.reservations.get(order_id)
        if not reservation:
            print(f"[InventoryService] 无冻结记录,幂等返回: {order_id}")
            return
        if reservation.status == ReservationStatus.RELEASED:
            print(f"[InventoryService] 库存已解冻,幂等返回: {order_id}")
            return

        self.available_stock += reservation.quantity
        reservation.status = ReservationStatus.RELEASED
        print(f"[InventoryService] 解冻库存成功: {order_id}, 剩余库存={self.available_stock}")


class PaymentService:
    def __init__(self, should_fail: bool = False):
        self.should_fail = should_fail
        self.paid_orders = set()

    def pay(self, order_id: str, amount: float):
        if order_id in self.paid_orders:
            print(f"[PaymentService] 支付已处理,幂等返回: {order_id}")
            return

        if self.should_fail:
            raise Exception("支付失败:模拟通道超时")

        self.paid_orders.add(order_id)
        print(f"[PaymentService] 支付成功: {order_id}, amount={amount}")


@dataclass
class SagaState:
    saga_id: str
    order_id: str = ""
    current_step: str = "INIT"
    status: str = "RUNNING"
    error: str = ""


class OrderSagaOrchestrator:
    def __init__(self, order_service: OrderService, inventory_service: InventoryService, payment_service: PaymentService):
        self.order_service = order_service
        self.inventory_service = inventory_service
        self.payment_service = payment_service
        self.sagas: Dict[str, SagaState] = {}

    def create_order(self, user_id: str, product_id: str, quantity: int, amount: float) -> str:
        saga_id = str(uuid.uuid4())
        saga = SagaState(saga_id=saga_id)
        self.sagas[saga_id] = saga

        try:
            saga.current_step = "CREATE_ORDER"
            order_id = self.order_service.create_order(user_id, amount)
            saga.order_id = order_id

            saga.current_step = "RESERVE_STOCK"
            self.inventory_service.reserve_stock(order_id, product_id, quantity)

            saga.current_step = "PAY"
            self.payment_service.pay(order_id, amount)

            saga.current_step = "CONFIRM_ORDER"
            self.order_service.confirm_order(order_id)

            saga.status = "SUCCESS"
            print(f"[Saga] 事务完成: saga_id={saga_id}")
            return order_id

        except Exception as e:
            saga.status = "FAILED"
            saga.error = str(e)
            print(f"[Saga] 事务失败: saga_id={saga_id}, step={saga.current_step}, error={e}")
            self.compensate(saga)
            raise

    def compensate(self, saga: SagaState):
        print(f"[Saga] 开始补偿: saga_id={saga.saga_id}, failed_step={saga.current_step}")
        order_id = saga.order_id

        if not order_id:
            return

        if saga.current_step == "PAY":
            self.inventory_service.release_stock(order_id)
            self.order_service.cancel_order(order_id)

        elif saga.current_step == "RESERVE_STOCK":
            self.order_service.cancel_order(order_id)

        elif saga.current_step == "CONFIRM_ORDER":
            print("[Saga] 订单确认失败,不立即退款,等待重试或人工处理")

        print(f"[Saga] 补偿结束: saga_id={saga.saga_id}")


if __name__ == "__main__":
    print("=== 场景1:正常下单 ===")
    order_service = OrderService()
    inventory_service = InventoryService(initial_stock=10)
    payment_service = PaymentService(should_fail=False)
    orchestrator = OrderSagaOrchestrator(order_service, inventory_service, payment_service)

    order_id = orchestrator.create_order(
        user_id="u1001",
        product_id="p2001",
        quantity=2,
        amount=99.9
    )
    print("订单状态:", order_service.orders[order_id].status)
    print()

    print("=== 场景2:支付失败触发补偿 ===")
    order_service2 = OrderService()
    inventory_service2 = InventoryService(initial_stock=10)
    payment_service2 = PaymentService(should_fail=True)
    orchestrator2 = OrderSagaOrchestrator(order_service2, inventory_service2, payment_service2)

    try:
        orchestrator2.create_order(
            user_id="u1002",
            product_id="p2002",
            quantity=3,
            amount=199.9
        )
    except Exception as e:
        print("捕获异常:", e)

    print("库存剩余:", inventory_service2.available_stock)

运行后你会看到什么?

  • 正常场景下:订单创建、库存冻结、支付成功、订单确认
  • 失败场景下:支付失败后自动触发库存解冻和订单取消

这个例子故意做了两件事:

  1. 每个服务都实现了幂等
  2. 补偿动作不是简单地“反向调用”,而是有状态判断

这是 Saga 落地时最容易被忽视、但最决定成败的两个点。


状态机设计建议

如果你的订单流程越来越复杂,靠 if/else 会很快失控。
更好的办法是把 Saga 显式建模为状态机。

stateDiagram-v2
    [*] --> INIT
    INIT --> ORDER_CREATED
    ORDER_CREATED --> STOCK_RESERVED
    ORDER_CREATED --> ORDER_CANCELLED: 库存失败
    STOCK_RESERVED --> PAYMENT_DONE
    STOCK_RESERVED --> STOCK_RELEASED: 支付失败
    STOCK_RELEASED --> ORDER_CANCELLED
    PAYMENT_DONE --> ORDER_CONFIRMED
    PAYMENT_DONE --> MANUAL_RETRY: 订单确认失败
    ORDER_CONFIRMED --> [*]
    ORDER_CANCELLED --> [*]
    MANUAL_RETRY --> ORDER_CONFIRMED

为什么状态机重要?

因为在真实系统中,失败不是只有一种:

  • 调用超时,但对方其实成功了
  • 消息重复投递
  • 补偿执行一半失败
  • 服务重启导致 Saga 上下文丢失

有了状态机后,你才能明确回答这些问题:

  • 当前 Saga 可以重试吗?
  • 当前步骤是否允许补偿?
  • 补偿是否已经执行过?
  • 是否需要人工介入?

实战落地时的关键设计点

1. 幂等是底线,不是优化项

Saga 场景里,重复调用是常态,不是异常。

你至少要对以下动作做幂等:

  • 创建订单
  • 冻结库存
  • 解冻库存
  • 发起支付
  • 取消订单
  • 确认订单

常用做法:

  • 业务唯一键,如 order_id
  • 幂等表 / 去重表
  • 状态机判断
  • 乐观锁版本号

例如库存冻结时可以直接用 order_id 作为唯一键,确保同一订单不会冻结两次。


2. 补偿不是“撤销”,而是“业务纠正”

这是我很想强调的一点。
很多人刚接触 Saga,会把补偿理解成数据库里的回滚。但现实不是这样。

比如:

  • 优惠券核销后失败,补偿是“返还一张券”,不是回滚历史
  • 支付成功后取消,补偿是“发起退款”,不是撤销支付记录
  • 库存扣减失败,补偿是“释放冻结”,不是穿越回过去

所以补偿动作必须是显式业务操作,且要能独立审计。


3. 本地事务与消息发送要原子化

如果你要从同步 Saga 演进到“本地事务 + 事件驱动”,一定会遇到一个经典问题:

数据库提交成功了,但消息没发出去怎么办?

这时建议用 Outbox Pattern(本地消息表)

  1. 在本地事务中同时写业务数据和消息表
  2. 后台任务扫描消息表并投递到 MQ
  3. 消费成功后更新消息状态

这样能把“业务变更”和“事件待发送”绑定在同一个本地事务里。


常见坑与排查

下面这些坑,几乎每个做 Saga 的团队都会踩。

坑 1:调用超时就当失败,结果补偿把成功结果冲掉了

现象

支付接口超时,协调器认为失败,执行了解冻库存和取消订单;
但几秒后支付通道回调成功,出现“支付成功但订单已取消”。

根因

“超时”不等于“失败”,它只是“结果未知”。

排查方法

  • 查请求日志和 traceId
  • 查支付侧是否已落账
  • 查回调是否晚于 Saga 超时阈值
  • 查补偿是否在“未知状态”下过早执行

建议

对外部系统调用引入三态:

  • SUCCESS
  • FAILED
  • UNKNOWN

其中 UNKNOWN 进入查询或延迟重试,而不是立刻补偿。


坑 2:补偿动作非幂等,导致库存越补越多

现象

支付失败后,补偿任务重试两次,库存被解冻两次。

根因

解冻接口没有基于冻结记录状态做判断。

建议

  • 冻结记录表按 order_id 唯一
  • 解冻前校验状态是否为 RESERVED
  • 更新时使用条件更新
UPDATE stock_reservation
SET status = 'RELEASED'
WHERE order_id = 'o1001' AND status = 'RESERVED';

如果受影响行数为 0,说明已经处理过。


坑 3:补偿顺序错了

现象

先取消订单,再解冻库存;
后续运营系统根据已取消订单做清理,库存解冻消息又晚到,产生状态混乱。

建议

补偿顺序通常按正向操作的逆序执行:

  1. 先释放外部资源
  2. 再关闭业务单据
  3. 最后发出终态事件

坑 4:没有持久化 Saga 上下文

现象

服务重启后,不知道哪些单子执行到一半,哪些该补偿,哪些该重试。

建议

Saga 协调器不能只放内存状态,至少要持久化:

  • saga_id
  • 业务单号
  • 当前步骤
  • 步骤结果
  • 重试次数
  • 最后错误信息
  • 更新时间

坑 5:重试机制没有退避策略,雪崩更严重

现象

支付服务短时故障,协调器每秒重试上万次,把下游彻底压垮。

建议

使用指数退避 + 抖动:

  • 第 1 次:1s
  • 第 2 次:2s
  • 第 3 次:4s
  • 加随机抖动避免集群同时重试

安全/性能最佳实践

Saga 不只是正确性问题,到了线上,安全和性能同样重要。

安全最佳实践

1. 所有跨服务调用都要带业务身份与签名校验

尤其是补偿接口,如:

  • /release-stock
  • /cancel-order
  • /refund-payment

这些接口一旦被伪造调用,风险很大。建议:

  • 内网零信任鉴权
  • 服务间 mTLS
  • HMAC 签名
  • 细粒度 RBAC

2. 补偿接口要限制来源和重放

对关键补偿请求增加:

  • 请求唯一 ID
  • 时间戳
  • 过期时间
  • 防重放 nonce

3. 敏感数据不要写入 Saga 日志

例如:

  • 完整银行卡号
  • 支付 token
  • 用户实名信息

日志里保留必要业务键即可,如 order_idsaga_idtrace_id


性能最佳实践

1. 缩短同步阻塞链路

能异步的步骤尽量异步:

  • 营销积分发放
  • 通知消息推送
  • 发票申请
  • 数据埋点

不要把所有步骤都塞进主交易链路。

2. Saga 步骤要有明确超时

没有超时的分布式调用,本质上就是资源泄漏。

建议按业务分类设置:

  • 库存冻结:100~300ms
  • 支付发起:1~3s
  • 订单确认:200~500ms

3. 容量估算不要只看 QPS,要看“在途 Saga 数”

一个很容易漏掉的指标是:

在途 Saga 数 = 每秒新建 Saga × 平均完成时长

例如:

  • 下单峰值 2000 TPS
  • 平均 Saga 生命周期 8 秒

那么在途 Saga 数大约是:

2000 × 8 = 16000

这会直接影响:

  • 协调器状态存储容量
  • 定时扫描任务压力
  • 重试队列堆积
  • 补偿任务并发度

4. 监控要按 Saga 维度建设

至少监控这些指标:

  • Saga 成功率
  • 各步骤失败率
  • 补偿触发率
  • 平均完成时长
  • 超时分布
  • 人工介入数
  • 重试次数分位值

一套更实际的落地建议

如果你准备在生产环境上 Saga,我建议按下面节奏推进,而不是一口吃成胖子。

第一阶段:先做同步编排版

目标:

  • 流程清晰
  • 状态机明确
  • 幂等到位
  • 补偿可用

适合先把主流程跑通。

第二阶段:引入本地消息表和异步事件

目标:

  • 降低同步耦合
  • 缩短主链路时延
  • 支持最终一致的异步修复

适合把“非关键同步步骤”迁出去。

第三阶段:建设运维与审计体系

目标:

  • Saga 可观测
  • 可重试
  • 可人工介入
  • 可审计复盘

很多团队前两步做得还行,真正出问题时却发现没有控制台、没有状态追踪、没有人工补偿入口,最后只能手改数据库。这种方式在测试环境能活,在生产环境迟早出事故。


边界条件:什么场景不适合 Saga?

Saga 很实用,但并不是所有业务都适合。

以下场景要谨慎:

1. 无法定义补偿动作

如果一个动作成功后不可逆,且没有业务纠正手段,那 Saga 很难成立。

2. 强实时强一致要求极高

例如某些核心账务场景,要求任意时刻都不能短暂不一致,这类更适合 TCC 或更强约束的账务设计。

3. 外部系统不支持查询与幂等

如果支付通道既不支持结果查询,也不支持幂等调用,协调器会非常难做,超时后几乎没法判定真实结果。


总结

在微服务架构里,订单一致性最难的地方,不是“让所有服务像单库事务一样同时成功”,而是:

  • 接受分布式环境中的失败与延迟
  • 用状态机管理流程
  • 用补偿机制修正不一致
  • 用幂等和可观测性保证系统可恢复

如果你要把这篇文章里的内容落地,我建议优先抓住这 5 个点:

  1. 先选 Orchestration 方式实现 Saga
  2. 每个步骤和补偿都做幂等
  3. 把 Saga 上下文持久化,别只放内存
  4. 把超时视为未知,不要轻易直接补偿
  5. 先保证可恢复,再追求优雅

一句话概括:

Saga 不是让分布式事务“完美无缺”,而是让系统在不完美的现实里,依然能稳定收敛到正确状态。

如果你的订单系统正处在“微服务化后事务开始失控”的阶段,Saga 往往是那个既现实、又足够工程化的答案。


分享到:

上一篇
《Java 中基于 CompletableFuture 的异步编排实战:提升接口聚合性能与可维护性》
下一篇
《Spring Boot 中基于 Spring Cache + Redis 的多级缓存实战:一致性、穿透击穿防护与性能调优》