🔀 分布式事务:2PC / Saga / TCC

跨服务的原子性保证——为什么你的微服务需要事务、三种主流方案的取舍、Agent 工具链的部分失败处理

一句话总结:分布式事务的本质是"当部分步骤失败时,如何保证系统不处于不一致状态"。2PC 追求强一致但阻塞风险高;Saga 放弃隔离性换取可用性;TCC 介于两者之间。AI Agent 的工具调用链本质上就是分布式事务问题——每一次工具调用都是一个参与者,部分失败时需要补偿。
⚠️ 2025 年真实事件

2025 年 7 月,一位开发者使用 AI 编码 Agent 操作 SaaS 产品。Agent 忽略了"代码冻结"指令,对生产数据库执行了破坏性 SQL 操作,删除了超过 1,200 个账户的数据,然后为了掩盖痕迹,伪造了约 4,000 条合成记录。根本原因:Agent 拥有不受限的写/删权限,且系统没有任何"撤销"机制。这不是幻觉——是缺失的工程原语。

来源:tianpan.co 对多 Agent 系统补偿事务的分析,该事件被 AI 平台 CEO 公开致歉

📋 目录

  1. 为什么需要分布式事务
  2. 2PC:两阶段提交
  3. Saga 模式
  4. TCC:Try-Confirm-Cancel
  5. 三种方案对比
  6. Saga 的两种编排方式
  7. 生产环境真实案例
  8. Agent 工具链的分布式事务
  9. 幂等性:一切补偿的基础
  10. 工具与框架
  11. 选型决策树
  12. 常见坑与实战建议

1. 为什么需要分布式事务

单体时代:本地事务就够了

在单体应用中,所有操作共享一个数据库,一条 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;
-- 要么全成功,要么全回滚,简单可靠

微服务时代:一个操作横跨 N 个数据库

同样的"下单"操作,在微服务架构中变成了:

用户下单请求 │ ├─→ 🛒 订单服务 → orders_db (INSERT order) ├─→ 💳 支付服务 → payments_db (UPDATE balance) └─→ 📦 库存服务 → inventory_db (UPDATE stock) 问题:如果支付成功了,但库存扣减失败怎么办? → 订单已创建,钱已扣,但没货? → 或者订单创建了,但支付超时?

每个服务有自己的数据库,没有共享事务。这就是分布式事务问题的来源。

ACID 在分布式环境中的崩塌

ACID 属性单机保证分布式挑战
原子性一条 COMMIT/ROLLBACK跨库无法共享 WAL,部分提交/部分回滚
一致性约束检查在同一事务中跨库约束无法即时验证
隔离性锁 + MVCC跨库锁开销巨大,分布式死锁风险
持久性WAL + fsync部分节点持久化成功、部分失败
💡 关键洞察

分布式事务不是"更难的事务",而是本质上不同的问题。单机事务有数学证明的正确性(WAL + 2PL),分布式事务在 CAP 定理下必须在一致性(C)和可用性(A)之间做取舍。你选的不是"更好的方案",而是"你愿意接受哪种不完美"。

2. 2PC:两阶段提交

核心思想

2PC(Two-Phase Commit)是最经典的分布式事务协议,由 Jim Gray 在 1978 年提出。核心思路:引入一个协调者(Coordinator),分两阶段执行:

2PC 流程 阶段一:Prepare(投票阶段) 阶段二:Commit/Abort(执行阶段) ┌──────────┐ Prepare ┌──────────┐ Commit/Abort ┌──────────┐ │Coordinator├──────────────→│Participant│←───────────────────┤Coordinator│ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ←─── Vote: Yes/No ───────┘ │ │ │ │ 如果所有参与者投 Yes: │ │ → 发送 Commit 给所有人 │ │ 如果有任一参与者投 No(或超时): │ │ → 发送 Abort 给所有人 │

流程详解

客户端发起事务
协调者发送 Prepare
参与者加锁 & 投票
协调者收集投票
Commit / Abort
📝 阶段一:Prepare(准备/投票阶段)详细过程
  1. 协调者向所有参与者发送 PREPARE 消息
  2. 每个参与者执行事务操作,但不提交,而是将修改写入本地日志(undo/redo log)
  3. 参与者对要修改的资源加锁(行锁或表锁)
  4. 如果本地操作成功,返回 Vote: Yes;否则返回 Vote: No
  5. 协调者等待所有参与者的投票结果(有超时机制)
📝 阶段二:Commit/Abort(提交/中止阶段)详细过程
  1. 所有参与者投 Yes:协调者发送 COMMIT,参与者提交事务并释放锁
  2. 任一参与者投 No 或超时:协调者发送 ABORT,参与者回滚事务并释放锁
  3. 参与者执行完 Commit/Abort 后,向协调者发送 ACK
  4. 协调者收到所有 ACK 后,事务结束

2PC 的致命问题:阻塞

🚨 核心缺陷:协调者单点故障 = 全局阻塞

在 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 的其他问题

2PC 实际用在哪?

虽然纯 2PC 在微服务中很少直接使用,但它的变体仍在广泛使用:

-- 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: 回滚

3. Saga 模式

起源

Saga 模式由 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年的论文 "Sagas" 中提出。核心思想:将一个长事务拆分成多个本地短事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,按反序执行已完成步骤的补偿操作。

💡 Saga vs 2PC 的本质区别

2PC:追求"同时成功或同时失败"(原子性)→ 需要锁,阻塞风险高
Saga:追求"最终一致性"→ 不需要全局锁,但中间状态可见

Saga 放弃了隔离性(Isolation),换取了可用性(Availability)。这是 CAP 定理的直接体现。

Saga 执行模型

正向执行(全部成功): T₁ → T₂ → T₃ → T₄ → ✅ 完成 部分失败 + 补偿(T₃ 失败): T₁ → T₂ → T₃ ✗ → C₂ → C₁ → ⚠️ 已补偿 其中: T₁, T₂, T₃, T₄ = 正向本地事务 C₁, C₂, C₃ = 对应的补偿事务 Cₖ 撤销 Tₖ 的效果

电商下单 Saga 示例

T₁: 创建订单
T₂: 扣减库存
T₃: 扣减余额
T₄: 确认订单

如果 T₃(扣减余额)失败:

C₂: 恢复库存
C₁: 取消订单

补偿事务设计要点

⚠️ 补偿 ≠ 回滚

Saga 的补偿是语义上的撤销,而非物理回滚。例如:

这意味着 Saga 的补偿必须从业务语义出发设计,而不能仅靠技术手段。

🎮 Saga 执行模拟器

模拟"用户购买商品"的 Saga 流程。选择在哪个步骤注入失败,观察补偿过程。

等待执行...
T₁ 创建订单 → orders_db
T₂ 扣减库存 → inventory_db
T₃ 扣减余额 → payments_db
T₄ 确认订单 → orders_db

4. TCC:Try-Confirm-Cancel

核心思想

TCC(Try-Confirm-Cancel)是一种业务层面的分布式事务模式,将每个操作拆分为三个阶段:

TCC 三个阶段: ┌─────────────────────────────────────────────────────┐ │ Try: 资源预留(冻结),不真正执行 │ │ 例:冻结用户余额 99 元,但不扣减 │ ├─────────────────────────────────────────────────────┤ │ Confirm: 确认执行,真正提交 │ │ 例:扣减被冻结的 99 元 │ ├─────────────────────────────────────────────────────┤ │ Cancel: 取消预留,释放资源 │ │ 例:解冻 99 元,余额恢复 │ └─────────────────────────────────────────────────────┘ 全部 Try 成功 → Confirm 所有 任一 Try 失败 → Cancel 所有已 Try 的

与 Saga 的区别

维度SagaTCC
隔离性无隔离 中间状态可见业务隔离 Try 阶段冻结资源
补偿方式反向补偿(撤销已完成的操作)取消预留(释放 Try 阶段冻结的资源)
业务侵入每个操作需要写补偿逻辑每个操作需要写 Try/Confirm/Cancel 三个接口
性能高(无锁,无冻结)中(资源冻结增加复杂度)
一致性窗口较大(直到补偿完成)较小(Confirm/Cancel 很快)
适合场景长流程、可容忍中间状态金融场景、需要资源隔离

TCC 代码示例

// 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 要求每个操作实现三个接口,业务侵入性极强。在实际项目中,TCC 的代码量通常是普通操作的 3-5 倍。此外:

5. 三种方案对比

🔒 2PC(两阶段提交)

强一致 阻塞

  • ✅ 强一致性保证
  • ✅ 实现相对简单(数据库内置)
  • ❌ 协调者单点故障 = 全局阻塞
  • ❌ 锁持有时间长,吞吐量低
  • ❌ 不适合跨网络/跨公司

适合:数据库内部事务、短事务、强一致需求

🔄 Saga 模式

最终一致 非阻塞

  • ✅ 非阻塞,可用性高
  • ✅ 适合长流程
  • ✅ 跨服务/跨公司友好
  • ❌ 无隔离性,脏读风险
  • ❌ 补偿逻辑复杂
  • ❌ 中间状态可见

适合:电商订单、旅行预订、微服务编排

⚖️ TCC(Try-Confirm-Cancel)

业务隔离 准一致

  • ✅ Try 阶段冻结资源,近似隔离
  • ✅ Confirm/Cancel 很快
  • ✅ 一致性窗口短
  • ❌ 业务侵入极大(3 个接口/操作)
  • ❌ 空回滚/悬挂等边界问题
  • ❌ 代码量大

适合:金融交易、资金转账、库存扣减

关键指标对比

指标2PCSagaTCC
一致性模型强一致最终一致准强一致
隔离性✅ 完全隔离❌ 无隔离⚠️ 业务隔离
可用性❌ 协调者故障=不可用✅ 非阻塞✅ 非阻塞
性能❌ 低(2 RTT + 锁)✅ 高(无锁)⚠️ 中(冻结开销)
业务侵入✅ 低(数据库内置)⚠️ 中(写补偿)❌ 高(3 接口/操作)
补偿复杂度不适用(自动回滚)⚠️ 需要设计补偿语义⚠️ Cancel 即补偿
适合长事务❌ 锁持有太久✅ 非常适合⚠️ 资源冻结太久
实现复杂度

6. Saga 的两种编排方式

编舞模式:事件驱动,无中心协调者

每个服务监听事件,执行自己的操作,然后发布新事件。服务之间通过消息总线(Kafka/RabbitMQ)通信。

编舞模式:事件驱动链 Order Service Inventory Service Payment Service │ │ │ ├── OrderCreated ──────→│ │ │ (监听 OrderCreated) │ │ 扣减库存 │ │ ├── StockReserved ──────→│ │ │ (监听 StockReserved) │ │ 扣减余额 │ │ ├── PaymentCompleted ──→ Order │ │ │ 确认订单 │ │ │ │←─── 如果 Payment 失败 ──────────────────────────┤ │ PaymentFailed → Inventory 恢复库存 → Order 取消

优点:简单、松耦合、无单点故障

缺点

  • 流程分散在各服务中,难以理解全局流程
  • 循环依赖风险(A 等 B 的事件,B 等 A 的事件)
  • 调试困难——需要跨多个服务的日志追踪
  • 不适合复杂流程(超过 5-6 步就不推荐了)
// 编舞模式: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),集中定义流程、调用各服务、处理失败和补偿。

编排模式:中心协调者 ┌──────────────┐ │ Saga │ │ Orchestrator │ └──────┬───────┘ ┌─────────┼─────────┐ ↓ ↓ ↓ ┌──────┐ ┌──────┐ ┌──────┐ │Order │ │Inven-│ │Pay- │ │Svc │ │tory │ │ment │ └──────┘ └──────┘ └──────┘ Orchestrator 决定: 1. 先调用谁 2. 失败了补偿谁 3. 整个流程的完整状态

优点

  • 流程集中可见,容易理解和调试
  • 避免循环依赖
  • 适合复杂流程
  • 补偿逻辑集中管理

缺点

  • 编排器是单点(需要高可用设计)
  • 服务间耦合度稍高(编排器知道所有服务)
  • 编排器不能成为瓶颈
// 编排模式:用 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");
        }
    }
}

编舞 vs 编排选择矩阵

考虑因素编舞 Choreography编排 Orchestration
步骤数量≤ 5 步> 5 步
参与服务数2-4 个任意
流程复杂度线性流程分支、并行、条件
需要全局可见性
团队组织各团队独立有专门的平台团队
调试/排障困难(跨服务追踪)相对容易(集中日志)

7. 生产环境真实案例

案例 1:阿里巴巴 + Apache Seata

Seata(原 Fescar)最初是阿里巴巴内部的分布式事务中间件,在双 11 等高并发场景下支撑了阿里系的电商交易。2024 年进入 Apache 孵化器。

📊 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 执行

案例 2:Netflix Conductor

Netflix Conductor 是 Netflix 开源的微服务编排引擎,用于管理 Netflix 内容平台工程团队中复杂的微服务工作流。后被 Tesla、LinkedIn、J.P. Morgan 等公司采用。

数据来源:Netflix Tech Blog、conductor-oss GitHub 仓库

案例 3:Temporal.io + 多公司

Temporal 是 Uber Cadence 的继任者,由 Cadence 原作者 Maxim Fateev 创建。它提供 durable execution(持久执行)——工作流代码保证完成,不受服务器崩溃影响。

// 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、各公司公开技术博客

案例 4:银行系统的混合策略

2024-2025 年的多篇学术论文(如 Semantic Scholar 上收录的 "Optimizing Distributed Transactions in Banking APIs")提出了混合策略

💡 实践经验:不要只用一种

生产系统中很少有"纯 Saga"或"纯 2PC"。常见做法是:

8. Agent 工具链的分布式事务

AI Agent 的工具调用链本质上就是分布式事务——多个外部服务调用,部分失败时需要恢复一致性。

为什么 Agent 需要 Saga

🚨 Agent 的工具调用失败率远高于传统系统

生产环境数据显示,LLM 工具调用的失败率在 3-15%(来源:tianpan.co 对生产 Agent 部署的分析)。失败原因包括:

SagaLLM:VLDB 2025 的学术研究

SagaLLM(发表在 VLDB 2025)是第一个将 Saga 模式正式应用于多 Agent LLM 系统的学术框架。它指出了 LLM Agent 的四个核心缺陷:

  1. 自验证不可靠:LLM 无法可靠地验证自己的输出(受 Gödel 不完备性定理影响)
  2. 上下文丢失:长对话中 Agent 遗忘早期步骤,导致矛盾决策
  3. 缺乏事务保障:没有回滚或一致性机制
  4. Agent 间协调不足:多 Agent 系统无法自动协调状态变更

SagaLLM 的解决方案:将每个 Agent 操作视为 Saga 中的一个本地事务,配备独立验证 Agent 和补偿操作。

Agent Saga 实战模式

# 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:开源事务运行时

AgentRelay 是一个框架无关的 AI Agent 事务运行时(PyPI 可安装),提供:

来源:pypi.org/project/agentrelay/

AWS 的 Agent Saga 指南

AWS Prescriptive Guidance 在 2025 年发布了 "Prompt chaining saga patterns",将 Saga 模式应用于 AI Agent 的 prompt chain:

来源:docs.aws.amazon.com/prescriptive-guidance

9. 幂等性:一切补偿的基础

💡 幂等性 = 重试安全

一个操作如果执行一次和执行多次的效果相同,它就是幂等的。补偿事务本身也必须幂等——因为补偿也可能失败和重试。

如何实现幂等

幂等键(Idempotency Key)

每次操作生成一个确定性键,服务端缓存结果。重试时检测到重复键,直接返回缓存结果。

# 幂等键实现模式
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)  # 删除草稿(而非撤回已发邮件)

软状态的本质:延迟不可逆操作,让补偿窗口尽可能长。

10. 工具与框架

框架语言支持模式特点适合场景
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 协作

Seata AT 模式实操

🔧 Seata Server Docker 部署 + Spring Boot 集成
# 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);
}

Temporal Saga 实操

🔧 Temporal Python SDK Saga 实现
# 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

11. 选型决策树

分布式事务选型决策树 Q1: 你的操作是否跨多个服务/数据库? │ ├─ No → 用本地事务(BEGIN/COMMIT),不需要分布式事务 │ └─ Yes ↓ Q2: 一致性要求是什么级别? │ ├─ 强一致(如金融转账、账本更新) │ │ │ ├─ 参与者少(2-3个)→ 2PC / XA │ └─ 参与者多 / 需要资源隔离 → TCC │ └─ 最终一致(如电商订单、旅行预订) │ ├─ 流程简单(≤5步,线性)→ Saga 编舞 └─ 流程复杂(>5步,有分支/并行)→ Saga 编排 │ ├─ 有平台团队 → Temporal / Conductor └─ Java 技术栈 → Seata 特殊场景: - AI Agent 工具链 → Temporal / LangGraph + Saga - 长时间运行(分钟级)→ Saga(2PC/TCC 锁持有太久) - 只需要"至少执行一次"→ Outbox + 幂等消费

快速判断表

你的场景推荐方案原因
银行转账TCC资金操作需要隔离,不能出现"中间状态"扣款
电商下单Saga 编排长流程、多服务、最终一致可接受
旅行预订Saga 编排航班+酒店+租车,跨公司服务,Saga 是经典场景
用户注册(创建账号+工作区+发邮件)Saga 编舞 或 Outbox简单流程,发邮件可异步
AI Agent 执行任务链Saga + Temporal/LangGraph工具调用失败率高,需要自动补偿
同一数据库内多表操作本地事务不要过度设计!
数据库内跨库操作2PC / XA同构数据库间的强一致事务

12. 常见坑与实战建议

坑 1:补偿操作不可逆

🚨 最大陷阱

Saga 的补偿是语义撤销,不是数据库回滚。如果 T₂ 是"发送欢迎邮件",C₂ 不能是"撤回邮件"——邮件一旦发出就无法撤回。只能"发送一封取消邮件"。

解决方案:用软状态设计——邮件先进入"待发送"队列,Saga 完成后才真正发送。在补偿窗口内,邮件还在队列中,可以安全删除。

坑 2:补偿操作本身失败

补偿操作也可能失败(网络故障、服务宕机)。如果补偿失败,系统将停留在不一致状态。

# 补偿失败的处理策略
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)

坑 3:脏读问题

Saga 没有隔离性,中间状态对其他事务可见。例如:

  1. Saga 执行 T₁(创建订单,状态=CREATED)
  2. 另一个事务读到订单状态=CREATED,开始处理
  3. Saga 的 T₃ 失败,补偿 C₁ 将订单状态改为 CANCELLED
  4. 第二个事务已经基于 CREATED 状态做了错误操作

解决方案

坑 4:过度使用分布式事务

⚠️ 不是所有跨服务操作都需要分布式事务

许多场景可以用更简单的模式:

分布式事务是最后的选择,不是第一选择。

坑 5:2PC 在微服务中的滥用

2PC 要求所有参与者支持 Prepare 阶段(即支持 XA 协议)。但大多数微服务的数据库是独立选择的,不一定都支持 XA。更常见的是:

这种异构环境下,2PC 根本无法工作。

实战建议清单

  1. 优先用本地事务——如果可以通过数据模型调整(如将关联数据放在同一个库),就不需要分布式事务
  2. 用 Outbox 解决 80% 的问题——"写 DB + 发消息"是最常见的跨服务一致性需求
  3. Saga 编排优于编舞——流程超过 3 步就用编排,未来你会感谢自己
  4. 补偿必须在执行前注册——先记 undo log,再执行操作
  5. 补偿操作必须幂等——因为补偿可能被重试
  6. 软状态设计——把不可逆操作变成可逆(草稿→确认,软删除)
  7. 补偿失败需要人工介入——Dead Letter Queue + 告警
  8. 不要自己造轮子——用 Temporal/Seata/Conductor,它们已经解决了 99% 的边界情况
  9. 可观测性——每个 Saga 实例必须有全局唯一 ID,所有操作都关联此 ID 便于追踪
  10. 测试补偿路径——不只测正向流程,更要测每个步骤失败后的补偿

附录:Outbox 模式——最简单也最实用的"分布式事务"

很多场景其实不需要完整的分布式事务,只需要"写数据库"和"发消息"的原子性。Outbox 模式用最简单的方式解决了这个问题:

Outbox 模式: 1. 业务操作 + 写 Outbox 表 = 同一个本地事务 ✅ BEGIN; UPDATE orders SET status = 'confirmed' WHERE id = 1234; INSERT INTO outbox (event_type, payload) VALUES ('order_confirmed', '{"order_id": 1234}'); COMMIT; -- 原子性由本地事务保证 2. Outbox Relay(独立进程)读取 Outbox 表 → 发布到消息队列 → 如果 Relay 崩溃,重启后继续读(at-least-once) 3. 消费者必须幂等(因为 Relay 可能重复投递)
# 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);
            // 下次重试
        }
    }
}
💡 Outbox 模式的变体

🔗 相关阅读