背景与问题
只要系统一拆成微服务,分布式事务几乎迟早会找上门。
最典型的场景是:下单、扣库存、冻结余额、生成物流单、发优惠券。这些动作分散在不同服务里,每个服务都有自己的数据库。此时你再想用单体时代那套本地事务,一句 BEGIN; COMMIT; 全包住,基本就不现实了。
很多团队在这个阶段会遇到几类很像、但本质不同的问题:
- 订单创建成功了,但库存没扣成功
- 库存扣成功了,但支付服务超时了
- 支付其实成功了,但回调消息丢了,订单一直停留在“待支付”
- 补偿逻辑执行了两次,把库存“补多了”
- 一次链路失败后,业务状态卡在中间态,人工也不敢随便改
这类问题的共同点是:
- 跨服务、跨库
- 调用链长,失败点多
- 不能简单回滚
- 业务需要最终一致,而不是强一致
这时候,Saga 模式就很适合登场了。
但我想先说个结论:Saga 不是“用了就万事大吉”的模式,它更像一套需要你认真设计状态机、幂等、补偿语义和排查手段的工程方法。
我见过不少项目“名义上用了 Saga”,结果线上一出故障,还是靠人工 SQL 修数据。
这篇文章我就从排障和实战落地的角度讲,尽量带你走一遍:
- Saga 到底怎么工作
- 编排式和协同式怎么选
- 一套最小可运行代码怎么写
- 线上出故障时从哪里查
- 补偿为什么经常“不生效”或者“补错了”
- 性能和安全上要注意什么
背景中的典型业务模型
我们先用一个电商下单流程作为例子:
- 订单服务创建订单
- 库存服务扣减库存
- 支付服务冻结余额
- 全部成功后,订单改为
CONFIRMED - 任一步失败,则按逆序补偿:
- 释放余额
- 回补库存
- 取消订单
这不是严格 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 很快就会失控。
一个最小状态流转可以是:
PENDINGORDER_CREATEDINVENTORY_RESERVEDPAYMENT_RESERVEDCOMPLETEDCOMPENSATINGFAILEDCOMPENSATED
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:支付服务
流程:
- 创建订单
- 预留库存
- 冻结余额
- 成功则确认订单
- 失败则补偿
实战代码(可运行)
把下面内容保存为 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 状态可能显示
FAILED或COMPENSATING
常见原因
- 补偿接口调用失败,但没有重试
- 库存服务补偿接口非幂等,第一次成功、第二次报错,协调器误判
- 只记录了主流程日志,没有记录补偿日志
- “扣库存”不是预留而是真扣减,补偿语义做错了
定位路径
建议按这个顺序查:
- 查
saga_id对应的状态流转 - 查
inventory.reserve是否成功写入 - 查
inventory.release是否发起 - 查库存服务日志中
reservation_id是否存在 - 查是否发生重复补偿
止血方案
- 人工补一条
release操作前,先确认reservation_id是否仍占用 - 如果没有预留记录,不要直接
available + 1 - 先冻结自动重试,避免人工修复和程序重试相互打架
现象 2:支付明明扣了钱,Saga 却显示失败
表现
- 用户余额变少了
- 订单没有确认
- Saga 状态是
COMPENSATING或FAILED
常见原因
- 支付服务成功了,但响应超时,协调器认为失败
- 支付成功消息落库了,但回包丢失
- 支付接口没有幂等键,重试造成重复冻结
定位路径
排查重点不是“有没有收到成功响应”,而是:
支付服务内部最终是否落账成功。
建议检查:
reservation_id是否存在- 账户余额和冻结金额是否变化
- 支付服务访问日志里是否有超时
- 编排器是否在超时后触发补偿
止血方案
- 对支付类接口强制查询式确认:超时后先查状态,再决定补偿
- 不要仅凭 HTTP timeout 直接判定业务失败
- 给每一步设计
query status接口
这个点我特别想强调:
网络超时 != 业务失败。
很多分布式事务事故,根子都在这。
现象 3:补偿执行了两次
表现
- 库存回补多了
- 余额解冻多了
- 日志里能看到重复补偿请求
常见原因
- 协调器重试
- 消费者重复消费消息
- 接口超时后上游重发
- 补偿动作写成了“增量式更新”
正确设计
补偿接口应该围绕资源占用记录来做,而不是直接改总量。
例如:
- 错误:
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 很久不动
表现
- 业务已失败
- 系统长时间处于补偿中
- 运维不知道能不能重试
常见原因
- 补偿服务本身不可用
- 协调器崩溃,恢复后没做断点续跑
- Saga 状态持久化不完整
- 补偿依赖下游状态查询,但查询接口异常
定位路径
建议看 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 -> CONFIRMEDCREATED -> 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_id、biz_id、status建索引 - 状态日志和业务数据分离
4. 补偿重试要指数退避
补偿失败后别立即狂打下游。
建议策略:
- 第 1 次:10 秒
- 第 2 次:30 秒
- 第 3 次:1 分钟
- 第 4 次:5 分钟
- 超限后转人工
5. 监控不要只盯错误率
Saga 更有价值的指标是:
COMPENSATING数量FAILED数量- 平均补偿耗时
- 每一步成功率
- 超时率
- 重复补偿率
- 长时间卡单数
落地建议:什么时候适合 Saga,什么时候不适合
适合 Saga 的场景
- 订单、库存、优惠券、积分这类业务闭环
- 可以接受短暂中间态
- 各步骤可设计补偿动作
- 更关注可用性和吞吐,而非绝对实时一致
不太适合 Saga 的场景
- 强一致账务核心流水
- 补偿不可逆且代价极高
- 涉及多个外部不可控系统
- 每一步状态都必须对用户不可见
如果业务处在灰区,我的建议是:
先收缩事务边界,再考虑 Saga。
不要一遇到跨服务就机械套模式。
总结
Saga 模式的价值,不在于“让分布式事务 magically 变简单”,而在于它提供了一条可落地、可补偿、可排障的路线。
真正能不能用好,关键看这几件事有没有做到位:
- 把业务流程建成清晰状态机
- 每一步都设计幂等键和补偿动作
- 超时后先查真实状态,不要直接判失败
- 补偿基于资源占用记录,而不是直接改总量
- Saga 主表 + Step 明细表一定要落库
- 给卡在补偿中的事务留重试和人工介入入口
- 用故障注入测试补偿链路,而不是只测 happy path
如果你现在正准备上线第一版 Saga,我给一个很务实的建议:
- 先做编排式
- 先覆盖最核心的 2~3 个步骤
- 先把可观测性和人工修复工具补齐
- 再考虑事件化、异步化和平台化
因为在真实生产环境里,最难的往往不是“把流程跑通”,而是出故障后,你能不能判断当前到底发生了什么,并且安全地把系统拉回一致状态。
而这,正是 Saga 实战真正的分水岭。