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

《微服务架构下的分布式事务落地:基于 Saga 模式的设计、实现与故障处理实践》

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

背景与问题

微服务拆开以后,最先变复杂的,往往不是业务本身,而是“原来一个本地事务能搞定的事,现在跨了 3 个服务、4 张表、还带一个消息队列”。

一个典型场景:

  • 订单服务:创建订单
  • 库存服务:冻结库存
  • 支付服务:扣款
  • 营销服务:发优惠券或积分

在单体应用里,这些动作通常可以放进一个数据库事务里,失败就一起回滚。
但到了微服务架构里:

  • 服务之间是网络调用,不可靠
  • 数据各自持有,不能共享本地事务
  • 下游可能超时、重试、消息重复
  • 某一步成功了,后续失败时要“补偿”而不是“数据库回滚”

这时,很多团队会想到 2PC/XA,但真实落地里常见问题是:

  • 对数据库、中间件支持要求高
  • 性能损耗明显
  • 协调器成为瓶颈或故障点
  • 对云原生和异构系统不够友好

所以,Saga 模式几乎成了微服务分布式事务里更务实的选择。

但 Saga 不是“选了就完事”。真正难的是:

  1. 补偿动作怎么定义才可靠?
  2. 并发下怎么避免重复执行?
  3. 某个步骤超时了,到底是失败、处理中,还是成功但响应丢了?
  4. 补偿失败时怎么止血?
  5. 如何排查“订单取消了但库存没释放”这种线上事故?

这篇文章我会从故障排查和落地实践角度来讲,不只讲概念,而是带你走一遍:设计、实现、复现问题、定位路径、止血方案。


核心原理

什么是 Saga

Saga 的核心思想很朴素:

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

比如:

  1. 创建订单
  2. 冻结库存
  3. 扣减余额
  4. 完成订单

如果第 3 步扣款失败,则触发补偿:

  • 释放库存
  • 取消订单

Saga 的两种实现风格

1. Choreography(事件编排/无中心)

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

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

优点:

  • 去中心化,简单场景上手快

缺点:

  • 业务链路变长后难追踪
  • 状态散落在多个服务里
  • 排查故障时像“猜谜”

2. Orchestration(中心编排)

引入一个 Saga Orchestrator:

  • 由编排器决定下一步执行谁
  • 记录整个 Saga 的状态
  • 失败时统一触发补偿

优点:

  • 更适合复杂流程
  • 可观测性和排障体验更好

缺点:

  • 编排器本身需要高可用设计

对中级工程师和大多数业务团队来说,我更建议优先采用 Orchestration。原因很现实:线上出问题时,能不能快速定位,比“架构图看起来优雅”更重要。


Saga 的关键设计点

1. 正向动作与补偿动作必须成对设计

不是所有动作都能完美回滚。

例如:

  • 冻结库存 → 释放库存:适合补偿
  • 扣款 → 退款:不是严格回滚,而是一个新业务动作
  • 发短信 → 无法真正撤回,只能记录并容忍

所以在设计时要先分清:

  • 可逆操作:冻结/解冻、预占/释放
  • 近似补偿:扣款/退款、发券/撤券
  • 不可补偿操作:外部通知、第三方副作用

2. 补偿必须幂等

这是 Saga 成败的分水岭。

为什么?

  • 网络超时后,调用方可能重试
  • 消息队列可能投递重复消息
  • 编排器恢复后可能再次调度补偿
  • 人工介入时也可能重复触发

所以你要接受一个事实:重复执行是常态,不是异常。

3. 每一步都要有明确状态机

最怕的是代码里只有一个 success/fail,线上一出问题就看不懂到底卡在哪。

建议至少有这些状态:

  • PENDING
  • RUNNING
  • SUCCESS
  • FAILED
  • COMPENSATING
  • COMPENSATED

如果步骤级别更细,还可以拆成:

  • 执行中
  • 执行成功待确认
  • 补偿中
  • 补偿失败待重试

Saga 执行流

flowchart TD
    A[创建订单 Saga] --> B[步骤1: 创建订单]
    B -->|成功| C[步骤2: 冻结库存]
    B -->|失败| X[结束: 失败]

    C -->|成功| D[步骤3: 扣减余额]
    C -->|失败| C1[补偿: 取消订单]

    D -->|成功| E[步骤4: 完成订单]
    D -->|失败| D1[补偿: 释放库存]
    D1 --> D2[补偿: 取消订单]

    E -->|成功| F[结束: 成功]
    E -->|失败| E1[补偿: 退款/人工核查]

状态机视角

stateDiagram-v2
    [*] --> PENDING
    PENDING --> RUNNING
    RUNNING --> SUCCESS
    RUNNING --> FAILED
    FAILED --> COMPENSATING
    COMPENSATING --> COMPENSATED
    COMPENSATING --> COMPENSATION_FAILED
    COMPENSATION_FAILED --> COMPENSATING
    SUCCESS --> [*]
    COMPENSATED --> [*]

实战代码(可运行)

下面用一个可运行的 Python 示例模拟一个简单的 Saga 编排器:

  • 订单服务:创建/取消订单
  • 库存服务:冻结/释放库存
  • 支付服务:扣款/退款
  • 编排器:驱动流程、记录状态、失败时补偿

这个例子偏教学,但结构上已经能映射真实系统中的关键点:幂等、状态记录、补偿链、故障模拟

示例代码

from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, List, Dict, Optional
import uuid
import random


class StepStatus(str, Enum):
    PENDING = "PENDING"
    RUNNING = "RUNNING"
    SUCCESS = "SUCCESS"
    FAILED = "FAILED"
    COMPENSATING = "COMPENSATING"
    COMPENSATED = "COMPENSATED"


@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
    product_id: str
    amount: int
    logs: List[str] = field(default_factory=list)
    step_records: Dict[str, StepRecord] = field(default_factory=dict)
    idempotency_keys: set = field(default_factory=set)

    def log(self, message: str):
        self.logs.append(message)
        print(message)


class OrderService:
    def __init__(self):
        self.orders = {}

    def create_order(self, ctx: SagaContext):
        if ctx.order_id in self.orders:
            ctx.log(f"[OrderService] 订单已存在,幂等返回: {ctx.order_id}")
            return
        self.orders[ctx.order_id] = {"status": "CREATED", "amount": ctx.amount}
        ctx.log(f"[OrderService] 创建订单成功: {ctx.order_id}")

    def cancel_order(self, ctx: SagaContext):
        order = self.orders.get(ctx.order_id)
        if not order:
            ctx.log(f"[OrderService] 订单不存在,取消视为幂等成功: {ctx.order_id}")
            return
        if order["status"] == "CANCELLED":
            ctx.log(f"[OrderService] 订单已取消,幂等返回: {ctx.order_id}")
            return
        order["status"] = "CANCELLED"
        ctx.log(f"[OrderService] 取消订单成功: {ctx.order_id}")

    def complete_order(self, ctx: SagaContext):
        order = self.orders.get(ctx.order_id)
        if not order:
            raise RuntimeError("订单不存在,无法完成")
        if order["status"] == "COMPLETED":
            ctx.log(f"[OrderService] 订单已完成,幂等返回: {ctx.order_id}")
            return
        if order["status"] == "CANCELLED":
            raise RuntimeError("订单已取消,不能完成")
        order["status"] = "COMPLETED"
        ctx.log(f"[OrderService] 完成订单成功: {ctx.order_id}")


class InventoryService:
    def __init__(self):
        self.stock = {"product-1": 10}
        self.frozen = {}

    def reserve(self, ctx: SagaContext):
        key = (ctx.order_id, ctx.product_id)
        if key in self.frozen:
            ctx.log(f"[InventoryService] 库存已冻结,幂等返回: {key}")
            return
        available = self.stock.get(ctx.product_id, 0)
        if available <= 0:
            raise RuntimeError("库存不足")
        self.stock[ctx.product_id] -= 1
        self.frozen[key] = 1
        ctx.log(f"[InventoryService] 冻结库存成功: {key}")

    def release(self, ctx: SagaContext):
        key = (ctx.order_id, ctx.product_id)
        if key not in self.frozen:
            ctx.log(f"[InventoryService] 无冻结记录,释放视为幂等成功: {key}")
            return
        self.stock[ctx.product_id] += 1
        del self.frozen[key]
        ctx.log(f"[InventoryService] 释放库存成功: {key}")


class PaymentService:
    def __init__(self):
        self.balance = {"user-1": 100}
        self.paid = {}

    def charge(self, ctx: SagaContext, fail_random=False):
        key = (ctx.order_id, ctx.user_id)
        if key in self.paid:
            ctx.log(f"[PaymentService] 已扣款,幂等返回: {key}")
            return

        if fail_random and random.choice([True, False]):
            raise RuntimeError("模拟支付通道超时")

        if self.balance.get(ctx.user_id, 0) < ctx.amount:
            raise RuntimeError("余额不足")

        self.balance[ctx.user_id] -= ctx.amount
        self.paid[key] = ctx.amount
        ctx.log(f"[PaymentService] 扣款成功: {key}, amount={ctx.amount}")

    def refund(self, ctx: SagaContext):
        key = (ctx.order_id, ctx.user_id)
        amount = self.paid.get(key)
        if amount is None:
            ctx.log(f"[PaymentService] 无扣款记录,退款视为幂等成功: {key}")
            return
        self.balance[ctx.user_id] += amount
        del self.paid[key]
        ctx.log(f"[PaymentService] 退款成功: {key}, amount={amount}")


@dataclass
class SagaStep:
    name: str
    action: Callable[[SagaContext], None]
    compensation: Optional[Callable[[SagaContext], None]] = None


class SagaOrchestrator:
    def __init__(self, steps: List[SagaStep]):
        self.steps = steps

    def execute(self, ctx: SagaContext):
        completed_steps = []

        for step in self.steps:
            ctx.step_records[step.name] = StepRecord(name=step.name)
            record = ctx.step_records[step.name]

            try:
                record.status = StepStatus.RUNNING
                ctx.log(f"[Saga] 开始执行步骤: {step.name}")
                step.action(ctx)
                record.status = StepStatus.SUCCESS
                completed_steps.append(step)
                ctx.log(f"[Saga] 步骤成功: {step.name}")
            except Exception as e:
                record.status = StepStatus.FAILED
                record.error = str(e)
                ctx.log(f"[Saga] 步骤失败: {step.name}, error={e}")
                self.compensate(ctx, completed_steps)
                raise

    def compensate(self, ctx: SagaContext, completed_steps: List[SagaStep]):
        ctx.log("[Saga] 开始补偿流程")
        for step in reversed(completed_steps):
            if not step.compensation:
                continue
            record = ctx.step_records.get(step.name)
            try:
                if record:
                    record.status = StepStatus.COMPENSATING
                ctx.log(f"[Saga] 补偿步骤: {step.name}")
                step.compensation(ctx)
                if record:
                    record.status = StepStatus.COMPENSATED
                ctx.log(f"[Saga] 补偿成功: {step.name}")
            except Exception as e:
                ctx.log(f"[Saga] 补偿失败: {step.name}, error={e}")
                # 真实生产中这里应落库、告警、进入重试队列
                raise


def main():
    order_service = OrderService()
    inventory_service = InventoryService()
    payment_service = PaymentService()

    ctx = SagaContext(
        saga_id=str(uuid.uuid4()),
        order_id="order-1001",
        user_id="user-1",
        product_id="product-1",
        amount=30
    )

    steps = [
        SagaStep(
            name="create_order",
            action=order_service.create_order,
            compensation=order_service.cancel_order
        ),
        SagaStep(
            name="reserve_inventory",
            action=inventory_service.reserve,
            compensation=inventory_service.release
        ),
        SagaStep(
            name="charge_payment",
            action=lambda c: payment_service.charge(c, fail_random=True),
            compensation=payment_service.refund
        ),
        SagaStep(
            name="complete_order",
            action=order_service.complete_order,
            compensation=None
        )
    ]

    orchestrator = SagaOrchestrator(steps)

    try:
        orchestrator.execute(ctx)
        print("\n=== Saga 执行成功 ===")
    except Exception as e:
        print(f"\n=== Saga 执行失败: {e} ===")

    print("\n=== 最终订单状态 ===")
    print(order_service.orders)

    print("\n=== 最终库存状态 ===")
    print(inventory_service.stock, inventory_service.frozen)

    print("\n=== 最终余额状态 ===")
    print(payment_service.balance, payment_service.paid)

    print("\n=== 步骤状态 ===")
    for k, v in ctx.step_records.items():
        print(k, v.status, v.error)


if __name__ == "__main__":
    main()

如何运行

python saga_demo.py

你会看到两类结果:

  • 成功:订单完成,库存减少,余额扣减
  • 失败:支付步骤抛错,触发库存释放与订单取消

这段代码对应的真实落地含义

这个例子虽然简单,但几个点是“生产上真的要做”的:

1. 步骤状态要可持久化

示例里放在内存。生产中应落到数据库,比如:

  • saga_instance:记录一个 Saga 实例
  • saga_step:记录每个步骤状态
  • compensation_task:记录待补偿任务

2. 补偿动作必须是业务语义上的逆操作

不是简单“delete 一条数据”。

比如支付成功后失败,不能直接改数据库余额,而应走标准退款逻辑,留下审计记录。

3. 每个服务都要支持幂等键

例如:

  • order_id
  • saga_id + step_name
  • request_id

我当时踩过一个坑:编排器超时后重试,支付服务没有幂等校验,结果用户被扣了两次款。
这类事故只靠“上层少重试”根本挡不住,必须把幂等做在服务入口。


现象复现

既然文章偏 troubleshooting,就不能只讲“正确姿势”,还要讲故障怎么复现

故障 1:支付超时,但其实已经扣款成功

这是最典型、也最难受的一类。

场景:

  1. 编排器调用支付服务
  2. 支付服务已经完成扣款
  3. 响应在网络中丢失或超时
  4. 编排器认为支付失败,开始补偿
  5. 后面又触发退款,甚至出现状态错乱

复现方式

可以把 PaymentService.charge() 改成:

def charge(self, ctx: SagaContext, fail_random=False):
    key = (ctx.order_id, ctx.user_id)
    if key in self.paid:
        ctx.log(f"[PaymentService] 已扣款,幂等返回: {key}")
        return

    if self.balance.get(ctx.user_id, 0) < ctx.amount:
        raise RuntimeError("余额不足")

    self.balance[ctx.user_id] -= ctx.amount
    self.paid[key] = ctx.amount
    ctx.log(f"[PaymentService] 扣款成功: {key}, amount={ctx.amount}")

    raise RuntimeError("模拟:扣款后响应超时")

这时你会得到一个很真实的问题:

  • 支付实际成功
  • Saga 视角却认为失败
  • 补偿是否执行,要依赖查询支付最终状态而不是只看调用是否抛异常

正确思路

对外部或不稳定调用,步骤状态不要直接二元化为“成功/失败”,而应允许:

  • UNKNOWN
  • CONFIRMING

然后通过查询接口、回查任务、对账任务确认最终结果。


故障 2:补偿失败导致事务半悬挂

场景:

  1. 创建订单成功
  2. 冻结库存成功
  3. 支付失败
  4. 开始补偿
  5. 释放库存时又失败

这时系统会处于一种很尴尬的状态:

  • 订单取消了
  • 库存还冻结着
  • 用户没付钱
  • 客服收到投诉

这就是典型的半悬挂状态


定位路径

线上排查 Saga 问题,我建议按下面这条路径走,效率很高。

1. 先查 Saga 实例,而不是先翻业务日志

先回答三个问题:

  • 这个 Saga 的全局 ID 是什么?
  • 当前停在第几步?
  • 正在正向执行还是补偿?

生产中建议统一透传:

  • trace_id
  • saga_id
  • order_id
  • step_name

2. 看步骤状态时间线

你真正需要的是一条时间线:

  • 订单创建成功时间
  • 库存冻结成功时间
  • 支付调用发起时间
  • 支付响应超时时间
  • 补偿启动时间
  • 补偿完成/失败时间

有了时间线,很多“玄学问题”会立刻清楚:
到底是重试太快、补偿太早,还是下游成功回执太慢。


3. 查幂等表和去重记录

很多重复扣款、重复释放、重复取消,本质上不是业务逻辑错,而是:

  • 没做幂等
  • 幂等键不稳定
  • 幂等记录写入时机不对

例如:

  • 先执行业务,再写幂等表:有窗口期
  • 用随机 UUID 做幂等键:重试时变了,等于没做
  • 仅内存缓存去重:实例重启就失效

4. 检查消息队列与本地事务的一致性

很多 Saga 流程会和消息结合,比如步骤成功后发事件。
如果你是:

  1. 先提交本地事务
  2. 再发送 MQ 消息

那么只要 MQ 发送失败,就可能出现:

  • 本地已经成功
  • 下游永远没收到事件

这类问题建议用:

  • Outbox Pattern
  • 本地消息表 + 异步投递
  • 消息状态回查

5. 判断是“需要重试”还是“需要人工介入”

不是所有失败都适合无限自动重试。

适合自动重试:

  • 网络闪断
  • 下游暂时超时
  • 死锁/短暂资源竞争

适合人工介入:

  • 数据已错乱
  • 外部支付状态长期不一致
  • 补偿多次失败
  • 第三方接口无回查能力

典型时序图

sequenceDiagram
    participant O as Orchestrator
    participant OS as OrderService
    participant IS as InventoryService
    participant PS as PaymentService

    O->>OS: createOrder(orderId)
    OS-->>O: success

    O->>IS: reserve(orderId, productId)
    IS-->>O: success

    O->>PS: charge(orderId, userId, amount)
    Note over PS: 实际已扣款
    PS--xO: 响应超时/丢失

    O->>PS: queryPayment(orderId)
    PS-->>O: paid=true

    O->>OS: completeOrder(orderId)
    OS-->>O: success

这个图想说明一个关键点:
调用超时不等于业务失败。


常见坑与排查

下面这些坑,我基本都见过,甚至有些就是线上事故复盘里反复出现的。

坑 1:把补偿当成数据库回滚

很多人刚做 Saga 时,下意识会想:“失败就撤销前面的操作”。

但补偿不是数据库回滚,它是一个新的业务动作
这意味着:

  • 会有时延
  • 会失败
  • 会被重试
  • 可能与用户行为并发发生

排查方式

如果发现“补偿后数据还是不对”,先检查:

  • 补偿动作是否真的覆盖了业务副作用
  • 是否存在用户侧并发修改
  • 补偿动作是否幂等
  • 补偿的前置条件是否过严

坑 2:步骤顺序设计不合理,导致补偿成本很高

例如你把“真实扣款”放在前面,把“库存预占”放在后面,一旦库存不足,就要退款。
从业务体验上看,这会明显放大退款链路压力。

建议顺序

通常更稳妥的是:

  1. 创建订单
  2. 预占库存/额度
  3. 扣款
  4. 最终确认

原则是:越难补偿的动作越靠后。


坑 3:补偿和正向操作并发冲突

比如:

  1. 支付超时,编排器准备补偿
  2. 支付服务其实晚一点返回成功
  3. 一个线程在退款,一个线程在完成订单

最终可能出现:

  • 订单完成但又退款了
  • 库存释放后又发货了

排查方式

看有没有做这些保护:

  • 订单状态机 CAS 更新
  • 分布式锁或业务锁
  • 乐观锁版本号
  • “最终态不可逆”约束

坑 4:重试没有退避策略,造成雪崩

下游支付超时后,编排器立即重试 3 次,多个实例同时打爆支付服务,然后整个链路一起抖。

建议

使用指数退避:

  • 第 1 次:1s
  • 第 2 次:5s
  • 第 3 次:30s
  • 第 4 次:5min

并加抖动(jitter),避免同一时刻集中重试。


坑 5:没有“死信”和“人工修复”通道

很多团队代码里写了补偿失败重试,但没有上限。
结果一条异常任务每天重试几千次,日志刷满,问题却没人知道。

止血方案

出现补偿持续失败时:

  1. 暂停自动重试
  2. 写入死信队列或异常任务表
  3. 告警到值班人
  4. 提供后台工具执行人工补偿/重放
  5. 做对账修复

止血方案

当线上已经出现 Saga 失控,先别急着“修优雅”,要优先止损。

场景 1:重复扣款

立即止血

  • 关闭编排器对该步骤的自动重试
  • 在支付服务入口加幂等校验
  • 根据 order_id 拉取重复扣款明细
  • 批量触发退款

后续修复

  • 幂等键改为稳定业务键
  • 将“调用超时”与“业务失败”分离
  • 增加支付状态回查机制

场景 2:库存长期冻结

立即止血

  • 扫描超过阈值未完成的冻结记录
  • 根据订单最终状态批量释放
  • 暂时降低下游失败步骤的流量入口

后续修复

  • 引入冻结过期时间 TTL
  • 增加定时清理任务
  • 对补偿失败任务建立告警与人工处理台账

场景 3:订单状态错乱

例如订单显示“已取消”,但支付和库存又显示成功。

立即止血

  • 以最关键业务实体为主视图建立修复规则
    比如交易系统里,通常支付状态优先级更高
  • 对不一致订单先冻结后续操作
  • 防止继续发货、继续退款或继续记账

后续修复

  • 明确统一状态机
  • 所有服务状态变更必须带版本号
  • 做跨服务对账任务

安全/性能最佳实践

Saga 不是只关心一致性,安全和性能也很重要。

安全最佳实践

1. 补偿接口不要对外裸奔

补偿接口本质上是“逆向业务操作”,风险很高。必须做到:

  • 鉴权
  • 签名校验
  • 白名单或内网调用限制
  • 审计日志

否则一个误调用就可能批量退款、批量释放库存。

2. 敏感字段最小暴露

跨服务传递 Saga 上下文时,不要把用户敏感信息一路透传。
能传 user_id 就不要传手机号,能传引用就不要传明文卡号。

3. 审计日志要可追溯

至少记录:

  • 谁发起的操作
  • 哪个 Saga 实例
  • 哪个步骤
  • 请求参数摘要
  • 响应结果
  • 补偿触发原因

性能最佳实践

1. 避免长链路串行过多步骤

步骤太多会放大失败概率。经验上:

  • 能合并的本地操作尽量合并
  • 低价值副作用改为异步最终一致
  • 非核心步骤不要强行纳入主事务

2. Saga 状态存储要有索引

常见查询条件:

  • saga_id
  • business_id/order_id
  • status
  • next_retry_time

否则一到补偿堆积,后台扫描任务就会拖垮库。

3. 重试任务要限流

补偿重试本身也可能制造事故。建议:

  • 按服务维度限速
  • 按租户/业务线隔离
  • 对第三方接口设置熔断和降级

推荐的表结构思路

CREATE TABLE saga_instance (
    saga_id VARCHAR(64) PRIMARY KEY,
    business_id VARCHAR(64) NOT NULL,
    saga_type VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    current_step VARCHAR(64),
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

CREATE TABLE saga_step (
    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,
    error_msg VARCHAR(512),
    started_at TIMESTAMP NULL,
    finished_at TIMESTAMP NULL,
    UNIQUE KEY uk_saga_step (saga_id, step_name)
);

CREATE TABLE compensation_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    saga_id VARCHAR(64) NOT NULL,
    step_name VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    next_retry_time TIMESTAMP NULL,
    retry_count INT NOT NULL DEFAULT 0,
    error_msg VARCHAR(512),
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

方案边界与取舍

这一节很重要,因为 Saga 不是银弹。

适合 Saga 的场景

  • 业务允许短暂最终不一致
  • 每一步都能设计补偿或近似补偿
  • 更看重可用性与扩展性
  • 涉及多个独立服务和数据库

不适合 Saga 的场景

  • 强一致要求极高,且不能容忍任何中间态
  • 核心动作不可补偿
  • 第三方系统没有幂等、没有回查、没有补偿能力
  • 团队还没有基础设施支撑状态管理、重试、告警、对账

如果你的业务是“扣一次钱绝对不能错,错了也不能事后退款弥补”,那 Saga 可能就不是最佳答案。
这时要重新评估:

  • 是否应收敛服务边界
  • 是否保留局部单体事务
  • 是否引入更强事务中间件

总结

Saga 真正难的,不是“知道它是什么”,而是把它做成一个线上出问题也能收拾得住的系统。

如果你只记住几条,我建议是这几条:

  1. 优先选编排式 Saga,排障体验更好。
  2. 每个步骤都要有幂等能力,而且幂等键必须稳定。
  3. 调用超时不等于业务失败,要有状态回查机制。
  4. 补偿是新业务动作,不是数据库回滚
  5. 自动重试要有限度,失败任务要能进入人工处理通道。
  6. 必须建设可观测性trace_idsaga_id、步骤状态时间线、告警、对账,一个都别少。

最后给一个比较务实的落地顺序:

  • 第一步:先把状态机、幂等和补偿接口定义清楚
  • 第二步:做一个最小可用编排器
  • 第三步:补齐日志、监控、告警、死信、人工修复工具
  • 第四步:通过故障注入演练支付超时、消息重复、补偿失败
  • 第五步:上线后持续做对账和复盘

我自己的经验是:
能跑的 Saga 不难,出了故障还能稳定收口的 Saga,才算真的落地。


分享到:

上一篇
《Java开发踩坑实战:排查并修复线程池误用导致的接口超时与内存飙升》
下一篇
《安卓逆向实战:从 Frida 动态插桩到 SO 层关键参数定位与加密流程还原》