分布式事务:Seata、TCC 与 Saga 模式详解

深入理解分布式事务的 ACID 挑战与解决方案,掌握 Seata AT/TCC/Saga 模式、本地消息表与最大努力通知的实战技巧

在微服务架构中,一次业务操作往往跨越多个服务和数据库,传统单机事务的 ACID 保证无法直接适用。分布式事务成为企业级系统必须面对的复杂课题。

一、分布式事务的核心挑战

1.1 CAP 与 BASE 理论

理论核心观点对事务的影响
CAP一致性、可用性、分区容错性不可兼得通常选择 AP,牺牲强一致性
BASE基本可用、软状态、最终一致接受短暂不一致,追求最终一致

1.2 分布式事务 vs 本地事务

// 本地事务:简单直接
@Transactional
public void localOrder() {
    orderDao.save(order);      // DB1
    inventoryDao.deduct(sku);  // 同一DB
}

// 分布式事务:跨服务、跨数据库
public void distributedOrder() {
    orderService.createOrder(order);      // svc1 + DB1
    inventoryService.deduct(sku, qty);    // svc2 + DB2
    paymentService.charge(userId, amt);   // svc3 + DB3
}

二、Seata:开源分布式事务解决方案

2.1 Seata 架构组件

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  TM (事务管理器) │----│  RM (资源管理器) │----│  TC (协调中心)  │
│   @GlobalTransactional   │    │  各服务的 DataSourceProxy   │    │ 维护全局事务状态│
└──────────────┘    └──────────────┘    └──────────────┘

2.2 AT 模式(Automatic Transaction)

AT 模式基于 SQL 解析和反向补偿,对业务零侵入。

@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createOrder(OrderRequest req) {
    // 1. 创建订单(DB1)
    Order order = orderService.create(req);
    
    // 2. 扣减库存(远程调用 svc2)
    inventoryService.deduct(req.getSkuId(), req.getQuantity());
    
    // 3. 扣减余额(远程调用 svc3)
    paymentService.debit(req.getUserId(), req.getAmount());
    
    return order;
}

AT 模式原理

  1. 一阶段:RM 拦截并解析 SQL,在业务 SQL 执行前后记录数据快照(before image / after image)到 undo_log 表。
  2. 二阶段-提交:若全局事务成功,TC 通知各 RM 异步删除 undo_log
  3. 二阶段-回滚:若全局事务失败,TC 通知各 RM 根据 undo_log 执行反向 SQL 恢复数据。
-- undo_log 表结构(关键字段)
SELECT * FROM undo_log WHERE xid = 'xxx';
-- branch_id | xid | context | rollback_info(JSON格式快照)

AT 模式优缺点

优点缺点
业务零侵入并非所有 SQL 都支持(如批量更新复杂 join)
自动回滚补偿需要额外存储 before/after image
性能损耗较低一阶段锁定资源时间较长

2.3 TCC 模式(Try-Confirm-Cancel)

TCC 将业务操作拆分为三个阶段,需要业务层实现三个接口。

public interface InventoryTccAction {
    
    @TwoPhaseBusinessAction(name = "inventoryDeduct")
    boolean tryDeduct(@BusinessActionContextParameter(paramName = "skuId") String skuId,
                      @BusinessActionContextParameter(paramName = "qty") int qty);
    
    boolean commit(BusinessActionContext ctx);
    
    boolean rollback(BusinessActionContext ctx);
}

@Service
public class InventoryTccActionImpl implements InventoryTccAction {
    
    @Autowired
    private InventoryDao inventoryDao;
    
    @Override
    public boolean tryDeduct(String skuId, int qty) {
        // 检查库存并预留(冻结)
        return inventoryDao.freezeStock(skuId, qty) > 0;
    }
    
    @Override
    public boolean commit(BusinessActionContext ctx) {
        String skuId = ctx.getActionContext("skuId");
        int qty = Integer.parseInt(ctx.getActionContext("qty"));
        // 确认扣减:将冻结库存转为实际扣减
        inventoryDao.confirmDeduct(skuId, qty);
        return true;
    }
    
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        String skuId = ctx.getActionContext("skuId");
        int qty = Integer.parseInt(ctx.getActionContext("qty"));
        // 释放冻结库存
        inventoryDao.unfreezeStock(skuId, qty);
        return true;
    }
}

TCC 注意事项

  • 幂等性:Confirm 和 Cancel 需保证幂等(网络超时可能重复调用)。
  • 空回滚:Try 未执行时,Cancel 不应报错(需记录事务状态)。
  • 悬挂:Cancel 先于 Try 执行,Try 需识别并跳过。

2.4 Saga 模式

Saga 将长事务拆分为一系列本地事务,每个本地事务提交后立即释放资源。

// 正向流程
public class OrderSaga {
    
    @Autowired private OrderService orderService;
    @Autowired private InventoryService inventoryService;
    @Autowired private PaymentService paymentService;
    
    public void start(OrderRequest req) {
        // Step 1: 创建订单
        orderService.create(req);
        
        // Step 2: 扣减库存
        inventoryService.deduct(req.getSkuId(), req.getQuantity());
        
        // Step 3: 扣款
        paymentService.charge(req.getUserId(), req.getAmount());
    }
    
    // 补偿流程(逆序执行)
    public void compensate(String orderId, String skuId, Long userId) {
        paymentService.refund(orderId);           // 退款
        inventoryService.restore(skuId);          // 恢复库存
        orderService.cancel(orderId);             // 取消订单
    }
}

Saga 的两种实现方式

类型说明适用场景
编排式 Saga(Choreography)各服务完成本地事务后发送事件,驱动下一个服务简单流程,服务间松耦合
编排式 Saga(Orchestration)中央协调器统一调度正向/补偿流程复杂流程,需要集中管控

三、本地消息表:最终一致性方案

本地消息表是业务侵入最小的最终一致性方案,核心思想是将分布式事务转化为本地事务 + 异步消息。

@Transactional
public void createOrder(OrderRequest req) {
    // 1. 保存订单
    orderDao.save(req.toOrder());
    
    // 2. 写入消息表(同一本地事务)
    Message message = Message.builder()
        .topic("order_created")
        .body(JsonUtils.toJson(req))
        .status("PENDING")
        .retryCount(0)
        .build();
    messageDao.save(message);
}

// 定时任务扫描消息表
@Scheduled(fixedRate = 5000)
public void scanPendingMessages() {
    List<Message> pending = messageDao.findByStatus("PENDING");
    for (Message msg : pending) {
        try {
            kafkaTemplate.send(msg.getTopic(), msg.getBody());
            msg.setStatus("SENT");
            messageDao.update(msg);
        } catch (Exception e) {
            msg.setRetryCount(msg.getRetryCount() + 1);
            if (msg.getRetryCount() > 3) {
                msg.setStatus("DEAD");  // 进入死信队列
            }
            messageDao.update(msg);
        }
    }
}

四、事务模式选型指南

模式一致性性能复杂度适用场景
Seata AT强一致简单 CRUD,无复杂 SQL
Seata TCC强一致高并发,需快速释放资源
Saga最终一致长事务,业务流程复杂
本地消息表最终一致异步场景,容忍延迟
最大努力通知最终一致对账场景,可人工介入

五、常见问题与最佳实践

5.1 避免分布式事务

最好的分布式事务是不使用分布式事务。

  • 服务拆分粒度合理:避免频繁跨服务事务。
  • 业务补偿替代事务:如发货失败触发退款流程。
  • 异步化:非核心操作改为消息异步处理。

5.2 监控与告警

# Seata TC 监控指标
seata:
  metrics:
    enabled: true
    registry-type: prometheus
    exporter-list: prometheus
    exporter-prometheus-port: 9898

5.3 超时与重试

@GlobalTransactional(
    name = "create-order",
    timeoutMills = 300000,    // 5分钟超时
    retryTimes = 3            // 重试次数
)

六、总结

方面建议
简单场景Seata AT 模式,低侵入
高性能场景TCC 模式,资源锁定时间短
长事务/工作流Saga 模式,分阶段提交
异步通知本地消息表,可靠性高
核心原则能不用就不用,避免过度设计

分布式事务没有银弹,理解业务场景的本质需求,选择合适的一致性级别,才是工程实践中的最佳策略。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析