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

《微服务架构中的分布式事务实战:基于 Saga 模式的设计、落地与故障补偿》

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

背景与问题

只要系统一拆成微服务,分布式事务几乎迟早会找上门。

最典型的场景是:下单、扣库存、冻结余额、生成物流单、发优惠券。这些动作分散在不同服务里,每个服务都有自己的数据库。此时你再想用单体时代那套本地事务,一句 BEGIN; COMMIT; 全包住,基本就不现实了。

很多团队在这个阶段会遇到几类很像、但本质不同的问题:

  • 订单创建成功了,但库存没扣成功
  • 库存扣成功了,但支付服务超时了
  • 支付其实成功了,但回调消息丢了,订单一直停留在“待支付”
  • 补偿逻辑执行了两次,把库存“补多了”
  • 一次链路失败后,业务状态卡在中间态,人工也不敢随便改

这类问题的共同点是:

  1. 跨服务、跨库
  2. 调用链长,失败点多
  3. 不能简单回滚
  4. 业务需要最终一致,而不是强一致

这时候,Saga 模式就很适合登场了。

但我想先说个结论:Saga 不是“用了就万事大吉”的模式,它更像一套需要你认真设计状态机、幂等、补偿语义和排查手段的工程方法。
我见过不少项目“名义上用了 Saga”,结果线上一出故障,还是靠人工 SQL 修数据。

这篇文章我就从排障和实战落地的角度讲,尽量带你走一遍:

  • Saga 到底怎么工作
  • 编排式和协同式怎么选
  • 一套最小可运行代码怎么写
  • 线上出故障时从哪里查
  • 补偿为什么经常“不生效”或者“补错了”
  • 性能和安全上要注意什么

背景中的典型业务模型

我们先用一个电商下单流程作为例子:

  1. 订单服务创建订单
  2. 库存服务扣减库存
  3. 支付服务冻结余额
  4. 全部成功后,订单改为 CONFIRMED
  5. 任一步失败,则按逆序补偿:
    • 释放余额
    • 回补库存
    • 取消订单

这不是严格 ACID,而是最终一致性事务

flowchart LR
    A[创建订单] --> B[扣减库存]
    B --> C[冻结余额]
    C --> D[订单确认]

    B -.失败.-> X1[取消订单]
    C -.失败.-> X2[回补库存]
    X2 --> X1

核心原理

什么是 Saga

Saga 可以理解成:把一个大事务拆成多个本地事务,每个本地事务成功后继续下一步;如果中间失败,就执行已经成功步骤对应的补偿操作。

比如:

  • 正向动作:扣库存

  • 补偿动作:回补库存

  • 正向动作:冻结余额

  • 补偿动作:解冻余额

注意,补偿不是数据库回滚,而是一个新的业务操作。
这点非常关键,也是很多 Bug 的根源。

两种常见实现方式

1. 编排式 Saga(Orchestration)

由一个中心协调者决定下一步做什么、失败后怎么补偿。

优点:

  • 流程清晰,适合复杂业务
  • 排障更直观
  • 可以集中记录 Saga 状态

缺点:

  • 协调器变复杂
  • 中心节点需要高可用设计

2. 协同式 Saga(Choreography)

各个服务通过事件驱动自行协作,没有中心调度者。

优点:

  • 解耦强
  • 天然适合事件架构

缺点:

  • 全链路不容易看清
  • 排障难,尤其步骤多时
  • 容易演变成“事件意大利面”

对于中级读者、尤其是正在做落地的团队,我通常建议:

先从编排式 Saga 开始。 不是因为它更“高级”,而是因为它更容易治理、排查和补偿。


Saga 的关键设计点

1. 状态机不是可选项

没有状态机,Saga 很快就会失控。

一个最小状态流转可以是:

  • PENDING
  • ORDER_CREATED
  • INVENTORY_RESERVED
  • PAYMENT_RESERVED
  • COMPLETED
  • COMPENSATING
  • FAILED
  • COMPENSATED
stateDiagram-v2
    [*] --> PENDING
    PENDING --> ORDER_CREATED
    ORDER_CREATED --> INVENTORY_RESERVED
    INVENTORY_RESERVED --> PAYMENT_RESERVED
    PAYMENT_RESERVED --> COMPLETED

    ORDER_CREATED --> COMPENSATING
    INVENTORY_RESERVED --> COMPENSATING
    PAYMENT_RESERVED --> COMPENSATING

    COMPENSATING --> COMPENSATED
    COMPENSATING --> FAILED

2. 补偿必须满足“业务可逆”,不是“技术回滚”

并不是所有操作都能补偿。

例如:

  • 发短信:通常不可逆,只能记录并接受副作用
  • 发券:可以再发一张撤销券,或者标记作废
  • 物流出库:一旦进入现实世界,补偿成本高很多

所以设计 Saga 前要问一句:

这个步骤失败后,我能否用业务动作把影响抵消?

如果不能,那它可能不适合放进 Saga,或者需要调整业务边界。

3. 幂等是底线

分布式环境里,超时、重试、重复投递都很常见。
因此以下动作都必须幂等:

  • 正向执行
  • 补偿执行
  • 状态更新
  • 消息消费

常见做法:

  • 每个请求带 sagaId / stepId
  • 业务表中保存操作日志或去重记录
  • 用唯一键约束避免重复写入
  • 补偿接口按“目标状态”设计,而不是“执行一次动作”

比如:

  • 不要设计成:库存+1
  • 更安全的是:释放 reservationId 对应的库存占用

方案落地:一套可运行的 Saga 示例

下面我用 Python + Flask 做一个最小可运行示例
为了便于本地演示,三个服务共用内存数据结构模拟数据库。真实项目里你应该替换成 MySQL/PostgreSQL + MQ + 持久化日志。

示例说明

包含四个角色:

  • orchestrator:Saga 编排器
  • order service:订单服务
  • inventory service:库存服务
  • payment service:支付服务

流程:

  1. 创建订单
  2. 预留库存
  3. 冻结余额
  4. 成功则确认订单
  5. 失败则补偿

实战代码(可运行)

把下面内容保存为 saga_demo.py,安装 Flask 后直接运行。

from flask import Flask, request, jsonify
import threading
import requests
import uuid
import time

app = Flask(__name__)

# 模拟数据库
orders = {}
inventory = {
    "sku-1": {"available": 10, "reservations": {}}
}
accounts = {
    "user-1": {"balance": 1000, "frozen": 0, "reservations": {}}
}
sagas = {}

# ========== 工具函数 ==========

def now():
    return int(time.time())

def ok(data=None):
    return jsonify({"success": True, "data": data or {}})

def fail(message, code=400):
    return jsonify({"success": False, "message": message}), code


# ========== Order Service ==========

@app.post("/order/create")
def order_create():
    data = request.json
    saga_id = data["saga_id"]
    order_id = data["order_id"]

    if order_id in orders:
        return ok({"order_id": order_id, "status": orders[order_id]["status"]})

    orders[order_id] = {
        "order_id": order_id,
        "user_id": data["user_id"],
        "sku": data["sku"],
        "amount": data["amount"],
        "status": "CREATED",
        "saga_id": saga_id,
        "created_at": now()
    }
    return ok({"order_id": order_id, "status": "CREATED"})


@app.post("/order/confirm")
def order_confirm():
    data = request.json
    order_id = data["order_id"]
    order = orders.get(order_id)
    if not order:
        return fail("order not found", 404)

    # 幂等
    if order["status"] == "CONFIRMED":
        return ok({"order_id": order_id, "status": "CONFIRMED"})

    if order["status"] == "CANCELLED":
        return fail("order already cancelled", 409)

    order["status"] = "CONFIRMED"
    return ok({"order_id": order_id, "status": "CONFIRMED"})


@app.post("/order/cancel")
def order_cancel():
    data = request.json
    order_id = data["order_id"]
    order = orders.get(order_id)
    if not order:
        return ok({"order_id": order_id, "status": "NOT_FOUND"})

    # 幂等
    if order["status"] == "CANCELLED":
        return ok({"order_id": order_id, "status": "CANCELLED"})

    if order["status"] != "CONFIRMED":
        order["status"] = "CANCELLED"
        return ok({"order_id": order_id, "status": "CANCELLED"})

    return fail("confirmed order cannot be cancelled directly", 409)


# ========== Inventory Service ==========

@app.post("/inventory/reserve")
def inventory_reserve():
    data = request.json
    reservation_id = data["reservation_id"]
    sku = data["sku"]
    qty = data["qty"]

    item = inventory.get(sku)
    if not item:
        return fail("sku not found", 404)

    # 幂等
    if reservation_id in item["reservations"]:
        return ok({"reservation_id": reservation_id, "status": "RESERVED"})

    if item["available"] < qty:
        return fail("insufficient inventory", 409)

    item["available"] -= qty
    item["reservations"][reservation_id] = qty
    return ok({"reservation_id": reservation_id, "status": "RESERVED"})


@app.post("/inventory/release")
def inventory_release():
    data = request.json
    reservation_id = data["reservation_id"]
    sku = data["sku"]

    item = inventory.get(sku)
    if not item:
        return ok({"reservation_id": reservation_id, "status": "NOT_FOUND"})

    qty = item["reservations"].pop(reservation_id, None)
    if qty is None:
        return ok({"reservation_id": reservation_id, "status": "ALREADY_RELEASED"})

    item["available"] += qty
    return ok({"reservation_id": reservation_id, "status": "RELEASED"})


# ========== Payment Service ==========

@app.post("/payment/reserve")
def payment_reserve():
    data = request.json
    reservation_id = data["reservation_id"]
    user_id = data["user_id"]
    amount = data["amount"]

    # 用 query 参数模拟故障注入
    force_fail = request.args.get("fail") == "1"

    account = accounts.get(user_id)
    if not account:
        return fail("user not found", 404)

    # 幂等
    if reservation_id in account["reservations"]:
        return ok({"reservation_id": reservation_id, "status": "RESERVED"})

    if force_fail:
        return fail("payment service forced failure", 500)

    if account["balance"] < amount:
        return fail("insufficient balance", 409)

    account["balance"] -= amount
    account["frozen"] += amount
    account["reservations"][reservation_id] = amount
    return ok({"reservation_id": reservation_id, "status": "RESERVED"})


@app.post("/payment/release")
def payment_release():
    data = request.json
    reservation_id = data["reservation_id"]
    user_id = data["user_id"]

    account = accounts.get(user_id)
    if not account:
        return ok({"reservation_id": reservation_id, "status": "NOT_FOUND"})

    amount = account["reservations"].pop(reservation_id, None)
    if amount is None:
        return ok({"reservation_id": reservation_id, "status": "ALREADY_RELEASED"})

    account["frozen"] -= amount
    account["balance"] += amount
    return ok({"reservation_id": reservation_id, "status": "RELEASED"})


# ========== Orchestrator ==========

def post_json(path, payload):
    resp = requests.post(f"http://127.0.0.1:5000{path}", json=payload, timeout=3)
    return resp.status_code, resp.json()

@app.post("/saga/create_order")
def create_order_saga():
    data = request.json
    saga_id = str(uuid.uuid4())
    order_id = str(uuid.uuid4())
    inventory_reservation_id = f"inv-{saga_id}"
    payment_reservation_id = f"pay-{saga_id}"

    sagas[saga_id] = {
        "saga_id": saga_id,
        "order_id": order_id,
        "status": "PENDING",
        "steps": []
    }

    try:
        code, res = post_json("/order/create", {
            "saga_id": saga_id,
            "order_id": order_id,
            "user_id": data["user_id"],
            "sku": data["sku"],
            "amount": data["amount"]
        })
        if code >= 400 or not res["success"]:
            raise Exception(f"order create failed: {res}")
        sagas[saga_id]["status"] = "ORDER_CREATED"
        sagas[saga_id]["steps"].append("order.create")

        code, res = post_json("/inventory/reserve", {
            "reservation_id": inventory_reservation_id,
            "sku": data["sku"],
            "qty": 1
        })
        if code >= 400 or not res["success"]:
            raise Exception(f"inventory reserve failed: {res}")
        sagas[saga_id]["status"] = "INVENTORY_RESERVED"
        sagas[saga_id]["steps"].append("inventory.reserve")

        payment_url = "/payment/reserve"
        if data.get("force_payment_fail"):
            payment_url += "?fail=1"

        resp = requests.post(
            f"http://127.0.0.1:5000{payment_url}",
            json={
                "reservation_id": payment_reservation_id,
                "user_id": data["user_id"],
                "amount": data["amount"]
            },
            timeout=3
        )
        res = resp.json()
        if resp.status_code >= 400 or not res["success"]:
            raise Exception(f"payment reserve failed: {res}")
        sagas[saga_id]["status"] = "PAYMENT_RESERVED"
        sagas[saga_id]["steps"].append("payment.reserve")

        code, res = post_json("/order/confirm", {
            "order_id": order_id
        })
        if code >= 400 or not res["success"]:
            raise Exception(f"order confirm failed: {res}")

        sagas[saga_id]["status"] = "COMPLETED"
        sagas[saga_id]["steps"].append("order.confirm")

        return ok({
            "saga_id": saga_id,
            "order_id": order_id,
            "status": "COMPLETED"
        })

    except Exception as e:
        sagas[saga_id]["status"] = "COMPENSATING"
        compensation_errors = []

        # 逆序补偿
        if "payment.reserve" in sagas[saga_id]["steps"]:
            try:
                post_json("/payment/release", {
                    "reservation_id": payment_reservation_id,
                    "user_id": data["user_id"]
                })
            except Exception as ce:
                compensation_errors.append(f"payment release failed: {ce}")

        if "inventory.reserve" in sagas[saga_id]["steps"]:
            try:
                post_json("/inventory/release", {
                    "reservation_id": inventory_reservation_id,
                    "sku": data["sku"]
                })
            except Exception as ce:
                compensation_errors.append(f"inventory release failed: {ce}")

        if "order.create" in sagas[saga_id]["steps"]:
            try:
                post_json("/order/cancel", {
                    "order_id": order_id
                })
            except Exception as ce:
                compensation_errors.append(f"order cancel failed: {ce}")

        if compensation_errors:
            sagas[saga_id]["status"] = "FAILED"
            return fail({
                "error": str(e),
                "compensation_errors": compensation_errors,
                "saga_id": saga_id
            }, 500)

        sagas[saga_id]["status"] = "COMPENSATED"
        return fail({
            "error": str(e),
            "saga_id": saga_id,
            "status": "COMPENSATED"
        }, 500)

@app.get("/debug/state")
def debug_state():
    return jsonify({
        "orders": orders,
        "inventory": inventory,
        "accounts": accounts,
        "sagas": sagas
    })

if __name__ == "__main__":
    app.run(debug=True)

运行方式

先安装依赖:

pip install flask requests

启动:

python saga_demo.py

成功场景测试

curl -X POST http://127.0.0.1:5000/saga/create_order \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user-1",
    "sku": "sku-1",
    "amount": 100
  }'

故障补偿测试

模拟支付服务失败:

curl -X POST http://127.0.0.1:5000/saga/create_order \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user-1",
    "sku": "sku-1",
    "amount": 100,
    "force_payment_fail": true
  }'

查看当前状态:

curl http://127.0.0.1:5000/debug/state

一次故障是怎么发生并被补偿的

下面这张时序图,对排障很有帮助。
很多线上问题,最后都是“流程以为失败了,但某一步其实成功了”这类认知偏差。

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

    Client->>O: 创建订单 Saga
    O->>OS: createOrder
    OS-->>O: success
    O->>IS: reserveInventory
    IS-->>O: success
    O->>PS: reservePayment
    PS-->>O: fail / timeout
    O->>IS: releaseInventory
    IS-->>O: success
    O->>OS: cancelOrder
    OS-->>O: success
    O-->>Client: compensated

现象复现:线上最常见的 4 类异常

这一部分是 troubleshooting 的重点。下面这些,我基本都在项目里见过。

现象 1:订单取消了,但库存没回补

表现

  • 订单状态是 CANCELLED
  • 库存却少了 1
  • Saga 状态可能显示 FAILEDCOMPENSATING

常见原因

  1. 补偿接口调用失败,但没有重试
  2. 库存服务补偿接口非幂等,第一次成功、第二次报错,协调器误判
  3. 只记录了主流程日志,没有记录补偿日志
  4. “扣库存”不是预留而是真扣减,补偿语义做错了

定位路径

建议按这个顺序查:

  1. saga_id 对应的状态流转
  2. inventory.reserve 是否成功写入
  3. inventory.release 是否发起
  4. 查库存服务日志中 reservation_id 是否存在
  5. 查是否发生重复补偿

止血方案

  • 人工补一条 release 操作前,先确认 reservation_id 是否仍占用
  • 如果没有预留记录,不要直接 available + 1
  • 先冻结自动重试,避免人工修复和程序重试相互打架

现象 2:支付明明扣了钱,Saga 却显示失败

表现

  • 用户余额变少了
  • 订单没有确认
  • Saga 状态是 COMPENSATINGFAILED

常见原因

  1. 支付服务成功了,但响应超时,协调器认为失败
  2. 支付成功消息落库了,但回包丢失
  3. 支付接口没有幂等键,重试造成重复冻结

定位路径

排查重点不是“有没有收到成功响应”,而是:

支付服务内部最终是否落账成功。

建议检查:

  • reservation_id 是否存在
  • 账户余额和冻结金额是否变化
  • 支付服务访问日志里是否有超时
  • 编排器是否在超时后触发补偿

止血方案

  • 对支付类接口强制查询式确认:超时后先查状态,再决定补偿
  • 不要仅凭 HTTP timeout 直接判定业务失败
  • 给每一步设计 query status 接口

这个点我特别想强调:
网络超时 != 业务失败。
很多分布式事务事故,根子都在这。


现象 3:补偿执行了两次

表现

  • 库存回补多了
  • 余额解冻多了
  • 日志里能看到重复补偿请求

常见原因

  1. 协调器重试
  2. 消费者重复消费消息
  3. 接口超时后上游重发
  4. 补偿动作写成了“增量式更新”

正确设计

补偿接口应该围绕资源占用记录来做,而不是直接改总量。

例如:

  • 错误:update inventory set available = available + 1
  • 正确:根据 reservation_id 删除一条占用记录,再把对应数量归还

建议 SQL 模式

create table inventory_reservation (
  reservation_id varchar(64) primary key,
  sku varchar(64) not null,
  qty int not null,
  status varchar(16) not null,
  created_at timestamp not null default current_timestamp
);

补偿时:

update inventory_reservation
set status = 'RELEASED'
where reservation_id = ? and status = 'RESERVED';

然后根据受影响行数判断是否已补偿过。


现象 4:Saga 卡在 COMPENSATING 很久不动

表现

  • 业务已失败
  • 系统长时间处于补偿中
  • 运维不知道能不能重试

常见原因

  1. 补偿服务本身不可用
  2. 协调器崩溃,恢复后没做断点续跑
  3. Saga 状态持久化不完整
  4. 补偿依赖下游状态查询,但查询接口异常

定位路径

建议看 3 份数据:

  • Saga 主表:当前状态、最后步骤、重试次数
  • Step 明细表:每个步骤执行结果、耗时、错误信息
  • 死信/重试队列:是否已经进入异步重试

止血方案

  • COMPENSATING 设置最大停留时长告警
  • 超过阈值进入人工审核队列
  • 支持“从某一步继续补偿”而不是整单重放

一个更接近生产的表设计建议

最小落地建议至少有两张表:

1. saga_instance

create table saga_instance (
  saga_id varchar(64) primary key,
  biz_type varchar(32) not null,
  biz_id varchar(64) not null,
  status varchar(32) not null,
  current_step varchar(64),
  retry_count int not null default 0,
  error_message text,
  created_at timestamp not null default current_timestamp,
  updated_at timestamp not null default current_timestamp
);

2. saga_step

create table saga_step (
  id bigint primary key auto_increment,
  saga_id varchar(64) not null,
  step_name varchar(64) not null,
  action_type varchar(16) not null,
  status varchar(32) not null,
  idempotency_key varchar(128) not null,
  request_payload text,
  response_payload text,
  error_message text,
  created_at timestamp not null default current_timestamp,
  updated_at timestamp not null default current_timestamp
);

有了这两张表,排障效率会提升非常明显。
至少你不会再面对一句模糊的报错:“订单创建失败,请稍后再试。”


常见坑与排查

这一节我把最容易踩坑的点集中列一下。

1. 把 Saga 当成 2PC 的替代品

Saga 解决的是最终一致性,不是强一致性。

如果你的业务要求:

  • 所有参与者必须同时成功/失败
  • 任何中间状态都不能暴露
  • 补偿成本极高甚至不可逆

那 Saga 可能不是最佳方案。
比如资金清算、核心账务,有时更适合用 TCC、串行化账本设计,甚至重新收缩服务边界。

2. 正向成功,补偿却不可逆

典型例子:

  • 已经通知外部合作方发货
  • 已经把短信发给用户
  • 已经调用第三方不可撤销接口

排查时常见误区是“为什么不回滚”。
答案是:业务上回不去。

解决方法不是硬补偿,而是:

  • 延迟不可逆动作到 Saga 最后
  • 先做可撤销预占
  • 外部调用尽量改成确认式两阶段

3. 只做接口幂等,不做状态幂等

很多服务接口看似幂等,但状态迁移不是。

比如一个订单:

  • CREATED -> CONFIRMED
  • CREATED -> CANCELLED

如果没有版本控制或状态校验,就可能出现并发覆盖。

建议:

  • 状态更新时带条件
  • 必要时增加版本号

示例 SQL:

update orders
set status = 'CONFIRMED'
where order_id = ? and status = 'CREATED';

4. 用同步串行调用硬撑整条链路

流程一长,超时概率指数上升。

如果每一步都同步阻塞:

  • 线程占用高
  • 调用方超时多
  • 用户体验差
  • 协调器容易雪崩

建议:

  • 用户入口尽快返回受理结果
  • Saga 后台异步推进
  • 前端轮询或订阅最终状态

5. 缺少故障注入测试

很多补偿逻辑“看起来能跑”,其实从没经过真实故障验证。

至少要测这些点:

  • 下游 500
  • 网络超时
  • 响应成功但丢包
  • 重复请求
  • 协调器重启
  • 补偿执行一半宕机

定位路径:我更推荐的排障顺序

线上真出问题时,不要一上来就改数据。
我更建议按下面顺序来:

flowchart TD
    A[拿到 bizId/orderId] --> B[查 saga_instance]
    B --> C[确认当前状态与最后步骤]
    C --> D[查 saga_step 明细]
    D --> E[查下游服务幂等记录]
    E --> F[确认真实业务状态]
    F --> G{是否需要补偿}
    G -- 是 --> H[执行定向补偿]
    G -- 否 --> I[修正 Saga 状态/推进流程]

具体检查项

第一步:确认 Saga 视角

看这些字段:

  • 当前状态
  • 最后成功步骤
  • 最后失败步骤
  • 重试次数
  • 是否正在补偿

第二步:确认业务真实状态

不要只看协调器记录。
还要去各服务分别确认:

  • 订单是否创建
  • 库存是否占用
  • 支付是否冻结
  • 是否已经补偿过

第三步:确认“错的是认知还是状态”

这一步非常重要。

有时是:

  • 业务已成功,但 Saga 状态没更新

有时是:

  • Saga 以为补偿成功,实际资源没释放

二者处理方式完全不同。


安全/性能最佳实践

Saga 经常被讨论成“事务模式”,但它本质上也是一套高并发、跨服务编排系统,所以安全和性能都不能忽略。

安全最佳实践

1. 补偿接口必须做权限隔离

补偿接口不是普通业务接口,应该只允许:

  • 编排器调用
  • 内部服务账户调用
  • 受控运维工具调用

不要把 /payment/release 这种接口裸露给外部。

建议:

  • mTLS 或服务间签名
  • 网关白名单
  • 独立 internal route
  • 操作审计日志

2. 幂等键不要可预测

如果幂等键可以被猜到,恶意请求可能触发重复查询或资源操作。

建议:

  • 使用 UUID / 雪花 ID
  • 绑定业务上下文
  • 存储调用来源与签名摘要

3. 敏感数据别进 Saga 日志

很多团队喜欢把 request/response 全量落表,后面排障确实方便,但也可能把这些写进去:

  • 手机号
  • 身份证
  • 银行卡信息
  • 支付凭证

建议:

  • 脱敏后再存
  • 日志分级
  • 对敏感字段加密或摘要化

性能最佳实践

1. 预留模式优于直接扣减模式

像库存、余额这类资源,尽量分成:

  • reserve
  • confirm / release

这样补偿成本低,幂等性也更好。

2. 长流程尽量异步化

如果用户请求要跨 5~8 个服务同步跑完,尾延迟会非常难看。

更稳妥的方式:

  • 用户提交后返回 PROCESSING
  • 编排器异步推进
  • 最终结果通过通知、轮询或事件回传

3. Saga 状态存储要能扛热点

热点业务下,同一张 Saga 表可能会高频更新。建议:

  • 按业务类型或时间分表
  • saga_idbiz_idstatus 建索引
  • 状态日志和业务数据分离

4. 补偿重试要指数退避

补偿失败后别立即狂打下游。

建议策略:

  • 第 1 次:10 秒
  • 第 2 次:30 秒
  • 第 3 次:1 分钟
  • 第 4 次:5 分钟
  • 超限后转人工

5. 监控不要只盯错误率

Saga 更有价值的指标是:

  • COMPENSATING 数量
  • FAILED 数量
  • 平均补偿耗时
  • 每一步成功率
  • 超时率
  • 重复补偿率
  • 长时间卡单数

落地建议:什么时候适合 Saga,什么时候不适合

适合 Saga 的场景

  • 订单、库存、优惠券、积分这类业务闭环
  • 可以接受短暂中间态
  • 各步骤可设计补偿动作
  • 更关注可用性和吞吐,而非绝对实时一致

不太适合 Saga 的场景

  • 强一致账务核心流水
  • 补偿不可逆且代价极高
  • 涉及多个外部不可控系统
  • 每一步状态都必须对用户不可见

如果业务处在灰区,我的建议是:

先收缩事务边界,再考虑 Saga。
不要一遇到跨服务就机械套模式。


总结

Saga 模式的价值,不在于“让分布式事务 magically 变简单”,而在于它提供了一条可落地、可补偿、可排障的路线。

真正能不能用好,关键看这几件事有没有做到位:

  1. 把业务流程建成清晰状态机
  2. 每一步都设计幂等键和补偿动作
  3. 超时后先查真实状态,不要直接判失败
  4. 补偿基于资源占用记录,而不是直接改总量
  5. Saga 主表 + Step 明细表一定要落库
  6. 给卡在补偿中的事务留重试和人工介入入口
  7. 用故障注入测试补偿链路,而不是只测 happy path

如果你现在正准备上线第一版 Saga,我给一个很务实的建议:

  • 先做编排式
  • 先覆盖最核心的 2~3 个步骤
  • 先把可观测性和人工修复工具补齐
  • 再考虑事件化、异步化和平台化

因为在真实生产环境里,最难的往往不是“把流程跑通”,而是出故障后,你能不能判断当前到底发生了什么,并且安全地把系统拉回一致状态

而这,正是 Saga 实战真正的分水岭。


分享到:

上一篇
《分布式架构中基于 Saga 模式的订单系统一致性设计与落地实践-389》
下一篇
《Docker 多阶段构建与镜像瘦身实战:从构建加速到生产环境安全发布-381》