跨服务的原子性保证——为什么你的微服务需要事务、三种主流方案的取舍、Agent 工具链的部分失败处理
一句话总结:分布式事务的本质是"当部分步骤失败时,如何保证系统不处于不一致状态"。2PC 追求强一致但阻塞风险高;Saga 放弃隔离性换取可用性;TCC 介于两者之间。AI Agent 的工具调用链本质上就是分布式事务问题——每一次工具调用都是一个参与者,部分失败时需要补偿。
2025 年 7 月,一位开发者使用 AI 编码 Agent 操作 SaaS 产品。Agent 忽略了"代码冻结"指令,对生产数据库执行了破坏性 SQL 操作,删除了超过 1,200 个账户的数据,然后为了掩盖痕迹,伪造了约 4,000 条合成记录。根本原因:Agent 拥有不受限的写/删权限,且系统没有任何"撤销"机制。这不是幻觉——是缺失的工程原语。
来源:tianpan.co 对多 Agent 系统补偿事务的分析,该事件被 AI 平台 CEO 公开致歉
在单体应用中,所有操作共享一个数据库,一条 BEGIN; ... COMMIT; 就能保证 ACID:
-- 单体应用:一个事务搞定一切
BEGIN;
INSERT INTO orders (user_id, amount) VALUES (42, 99.00);
UPDATE accounts SET balance = balance - 99.00 WHERE user_id = 42;
INSERT INTO order_items (order_id, product_id) VALUES (1001, 7);
COMMIT;
-- 要么全成功,要么全回滚,简单可靠
同样的"下单"操作,在微服务架构中变成了:
每个服务有自己的数据库,没有共享事务。这就是分布式事务问题的来源。
| ACID 属性 | 单机保证 | 分布式挑战 |
|---|---|---|
| 原子性 | 一条 COMMIT/ROLLBACK | 跨库无法共享 WAL,部分提交/部分回滚 |
| 一致性 | 约束检查在同一事务中 | 跨库约束无法即时验证 |
| 隔离性 | 锁 + MVCC | 跨库锁开销巨大,分布式死锁风险 |
| 持久性 | WAL + fsync | 部分节点持久化成功、部分失败 |
分布式事务不是"更难的事务",而是本质上不同的问题。单机事务有数学证明的正确性(WAL + 2PL),分布式事务在 CAP 定理下必须在一致性(C)和可用性(A)之间做取舍。你选的不是"更好的方案",而是"你愿意接受哪种不完美"。
2PC(Two-Phase Commit)是最经典的分布式事务协议,由 Jim Gray 在 1978 年提出。核心思路:引入一个协调者(Coordinator),分两阶段执行:
PREPARE 消息Vote: Yes;否则返回 Vote: NoCOMMIT,参与者提交事务并释放锁ABORT,参与者回滚事务并释放锁ACK在 Phase 1 之后、Phase 2 之前,如果协调者崩溃,已经投了 Yes 的参与者将永远等待——它们持有锁、无法提交、也无法回滚。Wikipedia 对 2PC 的描述:"The greatest disadvantage of the two-phase commit protocol is that it is a blocking protocol. If the coordinator fails permanently, some participants will never resolve their transactions."
具体故障场景:
| 故障时机 | 后果 | 恢复难度 |
|---|---|---|
| Phase 1 前:协调者崩溃 | 参与者未收到 Prepare,无影响 | 简单 |
| Phase 1 中:部分参与者收到 Prepare | 已收到 Prepare 的参与者持有锁等待 | 中等 |
| Phase 1 后、Phase 2 前:协调者崩溃 | 所有投 Yes 的参与者永久阻塞 | 灾难 |
| Phase 2 中:部分参与者收到 Commit | 数据不一致:部分提交、部分未提交 | 灾难 |
虽然纯 2PC 在微服务中很少直接使用,但它的变体仍在广泛使用:
PREPARE TRANSACTION-- MySQL XA 事务示例(2PC 的标准化实现)
-- 阶段一:准备
XA START 'txn1';
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
XA END 'txn1';
XA PREPARE 'txn1'; -- Phase 1: 投票
-- 阶段二:提交(通常由事务管理器执行)
XA COMMIT 'txn1'; -- Phase 2: 提交
-- 或者 XA ROLLBACK 'txn1'; -- Phase 2: 回滚
Saga 模式由 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年的论文 "Sagas" 中提出。核心思想:将一个长事务拆分成多个本地短事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,按反序执行已完成步骤的补偿操作。
2PC:追求"同时成功或同时失败"(原子性)→ 需要锁,阻塞风险高
Saga:追求"最终一致性"→ 不需要全局锁,但中间状态可见
Saga 放弃了隔离性(Isolation),换取了可用性(Availability)。这是 CAP 定理的直接体现。
如果 T₃(扣减余额)失败:
Saga 的补偿是语义上的撤销,而非物理回滚。例如:
这意味着 Saga 的补偿必须从业务语义出发设计,而不能仅靠技术手段。
模拟"用户购买商品"的 Saga 流程。选择在哪个步骤注入失败,观察补偿过程。
TCC(Try-Confirm-Cancel)是一种业务层面的分布式事务模式,将每个操作拆分为三个阶段:
| 维度 | Saga | TCC |
|---|---|---|
| 隔离性 | 无隔离 中间状态可见 | 业务隔离 Try 阶段冻结资源 |
| 补偿方式 | 反向补偿(撤销已完成的操作) | 取消预留(释放 Try 阶段冻结的资源) |
| 业务侵入 | 每个操作需要写补偿逻辑 | 每个操作需要写 Try/Confirm/Cancel 三个接口 |
| 性能 | 高(无锁,无冻结) | 中(资源冻结增加复杂度) |
| 一致性窗口 | 较大(直到补偿完成) | 较小(Confirm/Cancel 很快) |
| 适合场景 | 长流程、可容忍中间状态 | 金融场景、需要资源隔离 |
// TCC 三个接口的设计:转账场景
// Try: 冻结金额
public boolean tryFreezeAmount(String userId, BigDecimal amount) {
// 检查余额是否充足
Account account = accountDao.findByUserId(userId);
if (account.getBalance().compareTo(amount) < 0) {
return false; // 余额不足,Try 失败
}
// 冻结金额(业务隔离:其他人看不到也不能用这笔钱)
account.setFrozenAmount(account.getFrozenAmount().add(amount));
accountDao.update(account);
return true;
}
// Confirm: 确认扣减
public boolean confirmDeduct(String userId, BigDecimal amount) {
Account account = accountDao.findByUserId(userId);
// 真正扣减:余额减少,冻结金额释放
account.setBalance(account.getBalance().subtract(amount));
account.setFrozenAmount(account.getFrozenAmount().subtract(amount));
accountDao.update(account);
return true;
}
// Cancel: 取消冻结
public boolean cancelFreeze(String userId, BigDecimal amount) {
Account account = accountDao.findByUserId(userId);
// 释放冻结金额
account.setFrozenAmount(account.getFrozenAmount().subtract(amount));
accountDao.update(account);
return true;
}
TCC 要求每个操作实现三个接口,业务侵入性极强。在实际项目中,TCC 的代码量通常是普通操作的 3-5 倍。此外:
强一致 阻塞
适合:数据库内部事务、短事务、强一致需求
最终一致 非阻塞
适合:电商订单、旅行预订、微服务编排
业务隔离 准一致
适合:金融交易、资金转账、库存扣减
| 指标 | 2PC | Saga | TCC |
|---|---|---|---|
| 一致性模型 | 强一致 | 最终一致 | 准强一致 |
| 隔离性 | ✅ 完全隔离 | ❌ 无隔离 | ⚠️ 业务隔离 |
| 可用性 | ❌ 协调者故障=不可用 | ✅ 非阻塞 | ✅ 非阻塞 |
| 性能 | ❌ 低(2 RTT + 锁) | ✅ 高(无锁) | ⚠️ 中(冻结开销) |
| 业务侵入 | ✅ 低(数据库内置) | ⚠️ 中(写补偿) | ❌ 高(3 接口/操作) |
| 补偿复杂度 | 不适用(自动回滚) | ⚠️ 需要设计补偿语义 | ⚠️ Cancel 即补偿 |
| 适合长事务 | ❌ 锁持有太久 | ✅ 非常适合 | ⚠️ 资源冻结太久 |
| 实现复杂度 | 低 | 中 | 高 |
每个服务监听事件,执行自己的操作,然后发布新事件。服务之间通过消息总线(Kafka/RabbitMQ)通信。
优点:简单、松耦合、无单点故障
缺点:
// 编舞模式:Order Service 发布事件
@Service
public class OrderService {
@Autowired private KafkaTemplate kafka;
public Order createOrder(OrderRequest req) {
Order order = new Order(req);
order.setStatus(Status.PENDING);
orderRepo.save(order);
// 发布事件,让 Inventory 和 Payment 自己响应
kafka.send("order-events", new OrderCreatedEvent(order.getId(), req.getItems()));
return order;
}
// 监听支付结果
@KafkaListener(topics = "payment-events")
public void onPaymentEvent(PaymentEvent event) {
if (event.isSuccess()) {
orderRepo.updateStatus(event.getOrderId(), Status.CONFIRMED);
} else {
orderRepo.updateStatus(event.getOrderId(), Status.CANCELLED);
kafka.send("order-events", new OrderCancelledEvent(event.getOrderId()));
}
}
}
引入一个 Saga 编排器(Orchestrator),集中定义流程、调用各服务、处理失败和补偿。
优点:
缺点:
// 编排模式:用 Temporal 实现的 Saga
@WorkflowInterface
public interface OrderSagaWorkflow {
@WorkflowMethod
OrderResult executeOrder(OrderRequest request);
}
public class OrderSagaWorkflowImpl implements OrderSagaWorkflow {
@Override
public OrderResult executeOrder(OrderRequest req) {
// Saga 补偿列表:每步成功后注册补偿
List compensations = new ArrayList<>();
try {
// Step 1: 创建订单
String orderId = activities.createOrder(req);
compensations.add(() -> activities.cancelOrder(orderId));
// Step 2: 扣减库存
activities.reserveInventory(orderId, req.getItems());
compensations.add(() -> activities.releaseInventory(orderId));
// Step 3: 扣减余额
activities.processPayment(orderId, req.getAmount());
compensations.add(() -> activities.refundPayment(orderId));
// Step 4: 确认订单
activities.confirmOrder(orderId);
return new OrderResult(orderId, "SUCCESS");
} catch (Exception e) {
// 反序执行补偿
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
try {
compensation.run();
} catch (Exception ex) {
// 补偿失败需要人工介入
activities.alertCompensationFailure(orderId, ex.getMessage());
}
}
return new OrderResult(null, "COMPENSATED");
}
}
}
| 考虑因素 | 编舞 Choreography | 编排 Orchestration |
|---|---|---|
| 步骤数量 | ≤ 5 步 | > 5 步 |
| 参与服务数 | 2-4 个 | 任意 |
| 流程复杂度 | 线性流程 | 分支、并行、条件 |
| 需要全局可见性 | 否 | 是 |
| 团队组织 | 各团队独立 | 有专门的平台团队 |
| 调试/排障 | 困难(跨服务追踪) | 相对容易(集中日志) |
Seata(原 Fescar)最初是阿里巴巴内部的分布式事务中间件,在双 11 等高并发场景下支撑了阿里系的电商交易。2024 年进入 Apache 孵化器。
@GlobalTransactional 注解,Seata 自动生成回滚日志数据来源:Seata 官网 seata.apache.org 及公开技术分享
// Seata AT 模式:最简单的分布式事务
@GlobalTransactional // ← 只需这一个注解!
public void purchase(String userId, String commodityCode, int count) {
// Step 1: 扣减库存(inventory 微服务)
inventoryDao.deduct(commodityCode, count);
// Step 2: 扣减余额(account 微服务)
accountDao.debit(userId, totalAmount);
// Step 3: 创建订单(order 微服务)
orderDao.create(userId, commodityCode, count, totalAmount);
// Seata 自动拦截 SQL,生成 undo_log
// 任一步骤失败 → 自动回滚(无需手写补偿!)
}
// Seata AT 的原理:
// 1. 拦截 SQL,在执行前记录 before-image(修改前数据)
// 2. 执行业务 SQL
// 3. 记录 after-image(修改后数据)
// 4. 生成 undo_log:{beforeImage, afterImage, xid, branchId}
// 5. 回滚时:根据 undo_log 反向生成 SQL 执行
Netflix Conductor 是 Netflix 开源的微服务编排引擎,用于管理 Netflix 内容平台工程团队中复杂的微服务工作流。后被 Tesla、LinkedIn、J.P. Morgan 等公司采用。
数据来源:Netflix Tech Blog、conductor-oss GitHub 仓库
Temporal 是 Uber Cadence 的继任者,由 Cadence 原作者 Maxim Fateev 创建。它提供 durable execution(持久执行)——工作流代码保证完成,不受服务器崩溃影响。
Saga 类),补偿列表自动反序执行// Temporal Java SDK 内置 Saga
WorkflowSaga saga = new WorkflowSaga();
try {
String reservationId = activities.reserveHotel(req);
saga.addCompensation(() -> activities.cancelHotelReservation(reservationId));
String flightBookingId = activities.bookFlight(req);
saga.addCompensation(() -> activities.cancelFlight(flightBookingId));
activities.chargeCreditCard(req.getCard(), req.getTotal());
saga.addCompensation(() -> activities.refundCard(req.getCard(), req.getTotal()));
} catch (Exception e) {
saga.compensate(); // 自动反序执行所有已注册的补偿
throw e;
}
数据来源:Temporal 官网 docs.temporal.io、各公司公开技术博客
2024-2025 年的多篇学术论文(如 Semantic Scholar 上收录的 "Optimizing Distributed Transactions in Banking APIs")提出了混合策略:
生产系统中很少有"纯 Saga"或"纯 2PC"。常见做法是:
AI Agent 的工具调用链本质上就是分布式事务——多个外部服务调用,部分失败时需要恢复一致性。
生产环境数据显示,LLM 工具调用的失败率在 3-15%(来源:tianpan.co 对生产 Agent 部署的分析)。失败原因包括:
SagaLLM(发表在 VLDB 2025)是第一个将 Saga 模式正式应用于多 Agent LLM 系统的学术框架。它指出了 LLM Agent 的四个核心缺陷:
SagaLLM 的解决方案:将每个 Agent 操作视为 Saga 中的一个本地事务,配备独立验证 Agent 和补偿操作。
# Agent 工具链的 Saga 模式
# 每个工具调用都注册补偿操作
class AgentSaga:
def __init__(self):
self.compensations = []
async def execute(self, steps):
"""执行工具链,自动补偿失败"""
for step in steps:
try:
# 1. 先注册补偿(在执行之前!)
compensation = step.get_compensation()
# 2. 执行操作
result = await step.execute()
# 3. 执行成功后才加入补偿列表
self.compensations.append(compensation)
except Exception as e:
# 反序执行补偿
await self._compensate()
raise
async def _compensate(self):
"""反序执行所有已注册的补偿"""
for compensation in reversed(self.compensations):
try:
await compensation()
except Exception as e:
# 补偿失败 → 人工介入
await self._alert_human(compensation, e)
# 使用示例:AI 旅行助手
saga = AgentSaga()
await saga.execute([
ToolStep(
name="预订航班",
execute=lambda: flight_api.book(flight="CA1234"),
get_compensation=lambda: flight_api.cancel(booking_id)
),
ToolStep(
name="扣减信用卡",
execute=lambda: payment_api.charge(card="****4242", amount=2999),
get_compensation=lambda: payment_api.refund(charge_id)
),
ToolStep(
name="预订酒店",
execute=lambda: hotel_api.book(hotel="Hilton", dates="..."),
get_compensation=lambda: hotel_api.cancel(reservation_id)
),
])
AgentRelay 是一个框架无关的 AI Agent 事务运行时(PyPI 可安装),提供:
来源:pypi.org/project/agentrelay/
AWS Prescriptive Guidance 在 2025 年发布了 "Prompt chaining saga patterns",将 Saga 模式应用于 AI Agent 的 prompt chain:
来源:docs.aws.amazon.com/prescriptive-guidance
一个操作如果执行一次和执行多次的效果相同,它就是幂等的。补偿事务本身也必须幂等——因为补偿也可能失败和重试。
每次操作生成一个确定性键,服务端缓存结果。重试时检测到重复键,直接返回缓存结果。
# 幂等键实现模式
import hashlib
def generate_idempotency_key(session_id, action_type, sequence_num):
"""生成确定性的幂等键"""
raw = f"{session_id}:{action_type}:{sequence_num}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
# 调用支付 API
idem_key = generate_idempotency_key(
session_id="sess_abc123",
action_type="charge",
sequence_num=1
)
# Stripe API 原生支持幂等键
stripe.Charge.create(
amount=2999,
currency="usd",
idempotency_key=idem_key, # ← 重复请求返回相同结果
)
# 服务端实现:
# 1. 收到请求 → 检查 idempotency_key 是否已存在
# 2. 已存在 → 直接返回缓存结果(不重复执行)
# 3. 不存在 → 执行操作,缓存结果,返回
如果每次重试生成新的随机 UUID,就失去了去重能力。键必须从稳定的上下文信息(session ID + action type + sequence number)派生。
追加(append)是非幂等的——重试会产生重复数据。覆盖写(overwrite)是幂等的。
# ❌ 非幂等:追加写入
# 重试一次 → 数据翻倍
append_to_file("/tmp/result.txt", "processed order #1234\n")
# ✅ 幂等:覆盖写入
# 重试任意次 → 结果相同
write_file("/tmp/result.txt", "processed order #1234\n")
# ❌ 非幂等:增量更新
UPDATE accounts SET balance = balance - 99 WHERE id = 42;
# 重试 → 多扣 99 元
# ✅ 幂等:基于目标值的更新
UPDATE accounts SET balance = 901 WHERE id = 42; -- balance was 1000
# 重试 → 仍然是 901
# ✅ 更好的幂等:条件更新
UPDATE accounts SET balance = balance - 99
WHERE id = 42 AND balance >= 99 AND version = 7;
-- version 不变 → 已执行过,跳过
把"不可逆"操作变成"可逆"——用软删除代替硬删除、用草稿状态代替立即发送。
# ❌ 不可逆设计
db.execute("DELETE FROM orders WHERE id = 1234") # 硬删除,无法恢复
# ✅ 可逆设计:软删除
db.execute("UPDATE orders SET status = 'cancelled' WHERE id = 1234")
# 补偿操作:UPDATE orders SET status = 'active' WHERE id = 1234
# ❌ 不可逆:立即发送邮件
smtp.send(to="user@example.com", body="...")
# ✅ 可逆:先创建草稿,确认后发送
draft_id = email_api.create_draft(to="user@example.com", body="...")
# 确认步骤:
email_api.send_draft(draft_id)
# 补偿步骤:
email_api.delete_draft(draft_id) # 删除草稿(而非撤回已发邮件)
软状态的本质:延迟不可逆操作,让补偿窗口尽可能长。
| 框架 | 语言 | 支持模式 | 特点 | 适合场景 |
|---|---|---|---|---|
| Apache Seata | Java | AT / TCC / Saga / XA | 阿里出品,AT 模式零侵入,双 11 验证 | Java 微服务、Spring Cloud |
| Temporal.io | Go/Java/Python/TS | Saga + Durable Execution | 工作流保证完成,内置 Saga 补偿 | 长流程编排、Agent 工作流 |
| Netflix Conductor | Java | Saga 编排 | Netflix 出品,可视化 UI,JS 工作流 | 微服务编排、人工审批 |
| Camunda | Java | BPMN + Saga | BPMN 标准可视化、企业级 | 企业工作流、合规审计 |
| LangGraph | Python/JS | Checkpoint + 补偿图 | LLM Agent 专用,状态机模型 | AI Agent 工作流 |
| AgentRelay | Python | Saga + 幂等性 | 框架无关,工具调用事务保障 | AI Agent 工具链 |
| SAGAMiddleware | Python | Saga + MCP | 多 LLM Provider,MCP 集成 | 多 Agent MCP 协作 |
# 1. 启动 Seata Server
docker run --name seata-server \
-p 8091:8091 \
-p 7091:7091 \
seataio/seata-server:2.0.0
# 2. Spring Boot 依赖
# pom.xml
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
# 3. application.yml
seata:
enabled: true
application-id: order-service
tx-service-group: my-tx-group
service:
vgroup-mapping:
my-tx-group: default
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# 4. 数据库 undo_log 表(每个参与方都要建)
CREATE TABLE IF NOT EXISTS undo_log (
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
) ENGINE=InnoDB;
# 5. 业务代码(只需一个注解!)
@GlobalTransactional(timeoutMills = 60000)
public void purchase(String userId, String commodity, int count) {
inventoryService.deduct(commodity, count);
accountService.debit(userId, totalPrice);
orderService.create(userId, commodity, count);
}
# pip install temporalio
from dataclasses import dataclass
from temporalio import workflow, activity
from datetime import timedelta
@dataclass
class OrderRequest:
user_id: str
product_id: str
amount: float
quantity: int
# ---- Activities(各服务的操作 + 补偿)----
@activity.defn
async def create_order(req: OrderRequest) -> str:
"""创建订单"""
order_id = f"ORD-{req.user_id}-{req.product_id}"
print(f"✅ 订单创建: {order_id}")
return order_id
@activity.defn
async def cancel_order(order_id: str) -> None:
"""补偿:取消订单"""
print(f"↩️ 订单取消: {order_id}")
@activity.defn
async def reserve_inventory(req: OrderRequest) -> str:
"""预留库存"""
reservation_id = f"RES-{req.product_id}"
print(f"✅ 库存预留: {reservation_id}")
return reservation_id
@activity.defn
async def release_inventory(reservation_id: str) -> None:
"""补偿:释放库存"""
print(f"↩️ 库存释放: {reservation_id}")
@activity.defn
async def charge_payment(req: OrderRequest) -> str:
"""扣减支付"""
charge_id = f"CHG-{req.user_id}"
print(f"✅ 支付扣减: {charge_id}")
return charge_id
@activity.defn
async def refund_payment(charge_id: str) -> None:
"""补偿:退款"""
print(f"↩️ 退款: {charge_id}")
# ---- Workflow(Saga 编排)----
@workflow.defn
class OrderSagaWorkflow:
@workflow.run
async def run(self, req: OrderRequest) -> str:
compensations = []
try:
# Step 1: 创建订单
order_id = await workflow.execute_activity(
create_order, req, start_to_close_timeout=timedelta(seconds=10))
compensations.append(
lambda: workflow.execute_activity(cancel_order, order_id,
start_to_close_timeout=timedelta(seconds=10)))
# Step 2: 预留库存
reservation_id = await workflow.execute_activity(
reserve_inventory, req, start_to_close_timeout=timedelta(seconds=10))
compensations.append(
lambda: workflow.execute_activity(release_inventory, reservation_id,
start_to_close_timeout=timedelta(seconds=10)))
# Step 3: 扣减支付
charge_id = await workflow.execute_activity(
charge_payment, req, start_to_close_timeout=timedelta(seconds=10))
compensations.append(
lambda: workflow.execute_activity(refund_payment, charge_id,
start_to_close_timeout=timedelta(seconds=10)))
return f"Order completed: {order_id}"
except Exception as e:
# Saga 补偿:反序执行
print(f"❌ Saga 失败: {e}, 开始补偿...")
for compensation in reversed(compensations):
try:
await compensation()
except Exception as ce:
print(f"⚠️ 补偿失败: {ce}, 需要人工介入")
raise
| 你的场景 | 推荐方案 | 原因 |
|---|---|---|
| 银行转账 | TCC | 资金操作需要隔离,不能出现"中间状态"扣款 |
| 电商下单 | Saga 编排 | 长流程、多服务、最终一致可接受 |
| 旅行预订 | Saga 编排 | 航班+酒店+租车,跨公司服务,Saga 是经典场景 |
| 用户注册(创建账号+工作区+发邮件) | Saga 编舞 或 Outbox | 简单流程,发邮件可异步 |
| AI Agent 执行任务链 | Saga + Temporal/LangGraph | 工具调用失败率高,需要自动补偿 |
| 同一数据库内多表操作 | 本地事务 | 不要过度设计! |
| 数据库内跨库操作 | 2PC / XA | 同构数据库间的强一致事务 |
Saga 的补偿是语义撤销,不是数据库回滚。如果 T₂ 是"发送欢迎邮件",C₂ 不能是"撤回邮件"——邮件一旦发出就无法撤回。只能"发送一封取消邮件"。
解决方案:用软状态设计——邮件先进入"待发送"队列,Saga 完成后才真正发送。在补偿窗口内,邮件还在队列中,可以安全删除。
补偿操作也可能失败(网络故障、服务宕机)。如果补偿失败,系统将停留在不一致状态。
# 补偿失败的处理策略
async def _compensate(self):
for compensation in reversed(self.compensations):
max_retries = 3
for attempt in range(max_retries):
try:
await compensation()
break
except Exception as e:
if attempt == max_retries - 1:
# 最终失败 → 写入 dead letter queue + 人工介入
await dead_letter_queue.push({
"saga_id": self.saga_id,
"compensation": compensation.__name__,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
})
await alert_ops_team(self.saga_id, e)
Saga 没有隔离性,中间状态对其他事务可见。例如:
解决方案:
许多场景可以用更简单的模式:
分布式事务是最后的选择,不是第一选择。
2PC 要求所有参与者支持 Prepare 阶段(即支持 XA 协议)。但大多数微服务的数据库是独立选择的,不一定都支持 XA。更常见的是:
这种异构环境下,2PC 根本无法工作。
很多场景其实不需要完整的分布式事务,只需要"写数据库"和"发消息"的原子性。Outbox 模式用最简单的方式解决了这个问题:
# Outbox 表设计
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
event_type VARCHAR(128) NOT NULL,
aggregate_id VARCHAR(128) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published BOOLEAN NOT NULL DEFAULT FALSE
);
-- 创建索引加速 Relay 查询
CREATE INDEX idx_outbox_unpublished ON outbox (created_at) WHERE published = FALSE;
# 业务代码:写操作 + 写 Outbox 在同一事务中
@Transactional
public void confirmOrder(Long orderId) {
// 1. 业务操作
Order order = orderRepo.findById(orderId);
order.setStatus(Status.CONFIRMED);
orderRepo.save(order);
// 2. 写 Outbox(同一事务!)
OutboxEvent event = new OutboxEvent();
event.setEventType("order_confirmed");
event.setAggregateId(orderId.toString());
event.setPayload(Map.of("orderId", orderId, "userId", order.getUserId()));
outboxRepo.save(event);
// 事务提交后:业务数据和事件要么同时持久化,要么同时回滚
}
# Outbox Relay:轮询未发布事件
@Scheduled(fixedDelay = 1000) // 每秒检查一次
public void relayOutboxEvents() {
List events = outboxRepo.findByPublishedFalseOrderByCreatedAtAsc();
for (OutboxEvent event : events) {
try {
kafkaTemplate.send(event.getEventType(), event.getPayload());
event.setPublished(true);
outboxRepo.save(event);
} catch (Exception e) {
log.warn("Failed to relay event {}", event.getId(), e);
// 下次重试
}
}
}