🎮 Seata分布式事务框架:选择AT、TCC还是Saga?

98 阅读12分钟

面试官:你用过Seata吗?AT、TCC、Saga模式有什么区别?
候选人:用过AT模式...
面试官:为什么选AT而不是TCC?
候选人:😰💦(因为...简单?)

别慌!今天我们深入剖析Seata的三大模式,让你做选择时胸有成竹!


🎬 开篇:Seata是什么?

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案。

Seata = 一站式分布式事务解决方案

支持模式:
- AT模式(自动补偿)⭐⭐⭐⭐⭐ 最常用
- TCC模式(手动补偿)⭐⭐⭐⭐
- Saga模式(长事务)⭐⭐⭐
- XA模式(传统2PC)⭐⭐

架构图

           客户端应用
              │
    ┌─────────┼─────────┐
    │         │         │
  服务A     服务B     服务C
 (RM)      (RM)      (RM)
    │         │         │
    └─────────┼─────────┘
              │
         Seata Server
            (TC)

TC:Transaction Coordinator(事务协调者)
TM:Transaction Manager(事务管理器)
RM:Resource Manager(资源管理器)

🌟 第一章:AT模式 - 自动挡汽车(最简单!)

核心理念:自动补偿,业务无侵入

                 AT模式流程
                     
阶段1:业务数据和回滚日志记录在同一个本地事务中提交
       (自动记录前后快照)
                     ↓
阶段2:提交异步化,非常快
       成功:删除快照
       失败:用快照自动回滚

🎭 生活比喻:自动驾驶汽车

你:我要去超市买东西(开启全局事务)

自动驾驶系统:
1. 记录你的出发点(前镜像)
2. 开车去超市买东西(执行业务)
3. 记录购物后的状态(后镜像)
4. 如果出问题,自动开回原点(自动回滚)

你完全不用关心怎么回滚,系统自动帮你搞定!✨

💻 代码示例

// 订单服务
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockFeignClient stockClient;
    @Autowired
    private AccountFeignClient accountClient;
    
    /**
     * AT模式:只需要加一个注解!
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setAmount(dto.getAmount());
        orderMapper.insert(order); // Seata自动记录快照
        
        // 2. 远程调用:扣减库存
        stockClient.deduct(dto.getProductId(), dto.getQuantity());
        
        // 3. 远程调用:扣减余额
        accountClient.deduct(dto.getUserId(), dto.getAmount());
        
        // 如果任何一步失败,Seata自动回滚所有操作!
    }
}

// 库存服务(被调用方也不需要特殊代码)
@Service
public class StockService {
    
    @Autowired
    private StockMapper stockMapper;
    
    @Transactional // 普通的本地事务
    public void deduct(Long productId, Integer quantity) {
        Stock stock = stockMapper.selectById(productId);
        if (stock.getQuantity() < quantity) {
            throw new StockNotEnoughException(); // 异常会触发全局回滚
        }
        stock.setQuantity(stock.getQuantity() - quantity);
        stockMapper.updateById(stock); // Seata自动记录快照
    }
}

🔍 AT模式的工作原理

第一阶段:执行

-- 原始业务SQL
UPDATE stock SET quantity = quantity - 1 WHERE id = 1;

-- Seata在背后做的事:
-- 1. 查询前镜像(Before Image)
SELECT id, quantity FROM stock WHERE id = 1;
-- 结果:{id: 1, quantity: 100}

-- 2. 执行业务SQL
UPDATE stock SET quantity = quantity - 1 WHERE id = 1;

-- 3. 查询后镜像(After Image)
SELECT id, quantity FROM stock WHERE id = 1;
-- 结果:{id: 1, quantity: 99}

-- 4. 插入回滚日志
INSERT INTO undo_log (
    branch_id, xid, 
    before_image, after_image,
    rollback_info
) VALUES (
    '2001', 'global-tx-001',
    '{id: 1, quantity: 100}', -- 前镜像
    '{id: 1, quantity: 99}',  -- 后镜像
    '...'
);

-- 5. 提交本地事务(业务数据和undo_log一起提交)
COMMIT;

第二阶段:提交/回滚

// 成功场景:
TC向所有RM发送commit请求
→ RM删除undo_log
→ 完成

// 失败场景:
TC向所有RM发送rollback请求
→ RM根据undo_log的before_image恢复数据
→ 删除undo_log
→ 回滚完成

⚖️ AT模式的优缺点

✅ 优点

  1. 业务无侵入 👍👍👍

    // 只需要加一个注解,不用改业务代码!
    @GlobalTransactional
    public void business() {
        // 原有的业务代码
    }
    
  2. 自动补偿

    • 不需要写回滚逻辑
    • Seata自动生成反向SQL
  3. 性能较好

    • 第一阶段直接提交,释放锁
    • 第二阶段异步化

❌ 缺点

  1. 只支持关系型数据库(MySQL、Oracle等)

    • 不支持NoSQL(Redis、MongoDB)
  2. 有脏读风险

    时间线:
    T1: 事务A修改库存100→99(本地提交)
    T2: 事务B读到库存99(脏读!)
    T3: 事务A全局回滚,库存恢复100
    T4: 事务B基于99做决策(错误!)
    
  3. 对SQL有一定要求

    • WHERE条件必须包含主键或唯一索引
    • 不支持多表JOIN的UPDATE

📊 适用场景

✅ 适合:
- 基于关系型数据库的微服务
- 业务代码不想大改的项目
- 对性能有一定要求
- 能接受脏读风险

❌ 不适合:
- 使用NoSQL数据库
- 必须避免脏读
- SQL太复杂(大量JOIN)

💪 第二章:TCC模式 - 手动挡汽车(可控但复杂)

核心理念:手动补偿,完全可控

TCC = Try - Confirm - Cancel

Try:    尝试执行,预留资源(冻结)
Confirm:确认执行,使用资源(扣减)
Cancel: 取消执行,释放资源(解冻)

🎭 生活比喻:网购下单

Try阶段:
你:下单买手机
商家:给你保留一台(库存10099,冻结1)

Confirm阶段(支付成功):
商家:手机发货给你(真正扣减冻结的库存)

Cancel阶段(支付失败):
商家:释放保留的手机(解冻,库存恢复100

💻 代码示例

// 库存服务的TCC实现
@LocalTCC
public interface StockTccService {
    
    /**
     * Try:冻结库存
     * @param productId 商品ID
     * @param quantity 数量
     * @param businessKey 业务键(用于幂等)
     */
    @TwoPhaseBusinessAction(
        name = "stockTccService",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean freeze(
        @BusinessActionContextParameter(paramName = "productId") Long productId,
        @BusinessActionContextParameter(paramName = "quantity") Integer quantity,
        @BusinessActionContextParameter(paramName = "businessKey") String businessKey
    );
    
    /**
     * Confirm:确认扣减库存
     */
    boolean confirm(BusinessActionContext context);
    
    /**
     * Cancel:取消,释放冻结的库存
     */
    boolean cancel(BusinessActionContext context);
}

// TCC实现类
@Service
public class StockTccServiceImpl implements StockTccService {
    
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private FreezeLockMapper freezeLockMapper;
    
    /**
     * Try阶段:冻结库存
     */
    @Override
    @Transactional
    public boolean freeze(Long productId, Integer quantity, String businessKey) {
        // 幂等判断:检查是否已经冻结过
        FreezeLock existLock = freezeLockMapper.selectByBusinessKey(businessKey);
        if (existLock != null) {
            return true; // 已经冻结过,返回成功
        }
        
        // 1. 检查库存是否充足
        Stock stock = stockMapper.selectById(productId);
        int available = stock.getQuantity() - stock.getFrozen();
        if (available < quantity) {
            throw new StockNotEnoughException("库存不足");
        }
        
        // 2. 冻结库存(不减总库存,只增加冻结数)
        stock.setFrozen(stock.getFrozen() + quantity);
        stockMapper.updateById(stock);
        
        // 3. 记录冻结记录(用于Confirm和Cancel)
        FreezeLock lock = new FreezeLock();
        lock.setBusinessKey(businessKey);
        lock.setProductId(productId);
        lock.setQuantity(quantity);
        lock.setStatus("FROZEN");
        freezeLockMapper.insert(lock);
        
        return true;
    }
    
    /**
     * Confirm阶段:真正扣减库存
     */
    @Override
    @Transactional
    public boolean confirm(BusinessActionContext context) {
        String businessKey = context.getActionContext("businessKey").toString();
        
        // 幂等判断
        FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
        if (lock == null) {
            return true; // 已经处理过
        }
        if ("CONFIRMED".equals(lock.getStatus())) {
            return true; // 已经confirm过
        }
        
        // 1. 真正扣减库存
        Long productId = lock.getProductId();
        Integer quantity = lock.getQuantity();
        Stock stock = stockMapper.selectById(productId);
        
        stock.setQuantity(stock.getQuantity() - quantity); // 减总库存
        stock.setFrozen(stock.getFrozen() - quantity);     // 减冻结数
        stockMapper.updateById(stock);
        
        // 2. 更新冻结记录状态
        lock.setStatus("CONFIRMED");
        freezeLockMapper.updateById(lock);
        
        return true;
    }
    
    /**
     * Cancel阶段:释放冻结的库存
     */
    @Override
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        String businessKey = context.getActionContext("businessKey").toString();
        
        // 幂等判断
        FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
        if (lock == null) {
            return true; // 空回滚:Try还没执行,Cancel先来了
        }
        if ("CANCELLED".equals(lock.getStatus())) {
            return true; // 已经cancel过
        }
        
        // 1. 释放冻结库存
        Long productId = lock.getProductId();
        Integer quantity = lock.getQuantity();
        Stock stock = stockMapper.selectById(productId);
        
        stock.setFrozen(stock.getFrozen() - quantity); // 减冻结数(总库存不变)
        stockMapper.updateById(stock);
        
        // 2. 更新冻结记录状态
        lock.setStatus("CANCELLED");
        freezeLockMapper.updateById(lock);
        
        return true;
    }
}

// 业务调用
@Service
public class OrderService {
    
    @Autowired
    private StockTccService stockTccService;
    @Autowired
    private AccountTccService accountTccService;
    
    @GlobalTransactional
    public void createOrder(OrderDTO dto) {
        String businessKey = UUID.randomUUID().toString();
        
        // 调用TCC的Try方法
        stockTccService.freeze(dto.getProductId(), dto.getQuantity(), businessKey);
        accountTccService.freeze(dto.getUserId(), dto.getAmount(), businessKey);
        
        // Seata会自动调用Confirm或Cancel
    }
}

🛡️ TCC三大难题及解决方案

1️⃣ 空回滚问题

问题:Cancel在Try之前到达

时间线:
T1: 网络延迟,Try请求未到达
T2: 全局事务超时,TC发送Cancel请求
T3: Cancel执行(但Try还没执行!)
T4: Try请求终于到达
→ 结果:资源被冻结,永远释放不了!

解决

@Override
public boolean cancel(BusinessActionContext context) {
    String businessKey = context.getActionContext("businessKey").toString();
    FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
    
    if (lock == null) {
        // 空回滚:记录一条CANCELLED状态的记录,防止后续Try执行
        FreezeLock emptyLock = new FreezeLock();
        emptyLock.setBusinessKey(businessKey);
        emptyLock.setStatus("CANCELLED");
        freezeLockMapper.insert(emptyLock);
        return true;
    }
    
    // 正常回滚逻辑...
}

2️⃣ 幂等问题

问题:网络重试导致重复调用

T1: Try执行成功,冻结库存
T2: 网络超时,Seata重试
T3: Try又执行一次(重复冻结!)

解决

@Override
public boolean freeze(Long productId, Integer quantity, String businessKey) {
    // 检查businessKey是否已存在
    FreezeLock existLock = freezeLockMapper.selectByBusinessKey(businessKey);
    if (existLock != null) {
        return true; // 已经执行过,直接返回成功
    }
    
    // 正常冻结逻辑...
}

3️⃣ 悬挂问题

问题:Try在Cancel之后到达

时间线:
T1: Try请求网络延迟
T2: 全局事务超时,Cancel执行(空回滚)
T3: Try请求终于到达,执行成功
→ 结果:资源被冻结,但事务已经结束,永远无法释放!

解决

@Override
public boolean freeze(Long productId, Integer quantity, String businessKey) {
    FreezeLock lock = freezeLockMapper.selectByBusinessKey(businessKey);
    
    // 检查是否已经Cancel过(悬挂检测)
    if (lock != null && "CANCELLED".equals(lock.getStatus())) {
        return false; // 拒绝执行Try
    }
    
    // 正常冻结逻辑...
}

⚖️ TCC模式的优缺点

✅ 优点

  1. 性能最好

    • Try阶段提交,不长时间锁定
    • 无需记录回滚日志
  2. 完全可控

    • 自己写补偿逻辑
    • 可以支持NoSQL
  3. 无脏读问题

    • 其他事务读到的是冻结前的数据

❌ 缺点

  1. 开发成本高 👎👎👎

    • 每个操作都要写Try、Confirm、Cancel
    • 要处理空回滚、幂等、悬挂
  2. 数据库设计复杂

    • 需要冻结字段
    • 需要中间状态表
  3. 业务侵入性强

    • 必须修改业务代码

📊 适用场景

✅ 适合:
- 对性能要求极高
- 涉及NoSQL数据库
- 必须避免脏读
- 愿意付出高开发成本

❌ 不适合:
- 快速开发的项目
- 团队技术能力一般
- 业务逻辑频繁变化

🌊 第三章:Saga模式 - 长途旅行(适合长流程)

核心理念:正向服务+补偿服务

Saga = 一系列本地事务 + 补偿事务

正向:T1 → T2 → T3 → 成功
失败:T1 → T2 → T3失败 → C3 → C2 → C1

💻 代码示例(状态机模式)

// 定义Saga状态机(JSON格式)
{
  "Name": "OrderSaga",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "create",
      "CompensateState": "CancelOrder",
      "Next": "DeductStock"
    },
    "DeductStock": {
      "Type": "ServiceTask",
      "ServiceName": "stockService",
      "ServiceMethod": "deduct",
      "CompensateState": "RestoreStock",
      "Next": "DeductAccount"
    },
    "DeductAccount": {
      "Type": "ServiceTask",
      "ServiceName": "accountService",
      "ServiceMethod": "deduct",
      "CompensateState": "RestoreAccount",
      "Next": "Succeed"
    },
    "Succeed": {
      "Type": "Succeed"
    },
    "CancelOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "cancel"
    },
    "RestoreStock": {
      "Type": "ServiceTask",
      "ServiceName": "stockService",
      "ServiceMethod": "restore"
    },
    "RestoreAccount": {
      "Type": "ServiceTask",
      "ServiceName": "accountService",
      "ServiceMethod": "restore"
    }
  }
}
// 订单服务
@Service
public class OrderService {
    
    /**
     * 正向服务:创建订单
     */
    @Transactional
    public boolean create(BusinessActionContext context) {
        OrderDTO dto = (OrderDTO) context.getActionContext("dto");
        
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setAmount(dto.getAmount());
        order.setStatus(OrderStatus.CREATED);
        orderMapper.insert(order);
        
        return true;
    }
    
    /**
     * 补偿服务:取消订单
     */
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        String orderId = context.getActionContext("orderId").toString();
        
        Order order = orderMapper.selectById(orderId);
        order.setStatus(OrderStatus.CANCELLED);
        orderMapper.updateById(order);
        
        return true;
    }
}

// 业务调用
@Service
public class BusinessService {
    
    @Autowired
    private StateMachineEngine stateMachineEngine;
    
    public void createOrder(OrderDTO dto) {
        Map<String, Object> context = new HashMap<>();
        context.put("dto", dto);
        
        // 启动Saga状态机
        StateMachineInstance instance = stateMachineEngine.start(
            "OrderSaga", 
            null, 
            context
        );
        
        if (ExecutionStatus.SU.equals(instance.getStatus())) {
            System.out.println("订单创建成功!");
        } else {
            System.out.println("订单创建失败,已补偿!");
        }
    }
}

⚖️ Saga模式的优缺点

✅ 优点

  1. 适合长流程

    • 可以跨越很长时间
    • 不长时间锁定资源
  2. 业务侵入性中等

    • 只需要写补偿方法
    • 比TCC简单很多

❌ 缺点

  1. 只能保证最终一致性

    • 中间状态可能被看到
  2. 补偿逻辑可能很复杂

    • 某些操作难以补偿(如发送短信)

📊 适用场景

✅ 适合:
- 长流程业务(订单流程、工单流程)
- 对一致性要求不那么严格
- 需要异步处理

❌ 不适合:
- 短流程
- 需要强一致性

🎯 第四章:三种模式全方位对比

维度AT模式TCC模式Saga模式
一致性最终一致强一致最终一致
隔离性读未提交(脏读)读已提交读未提交
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
业务侵入⭐(很小)⭐⭐⭐⭐⭐(很大)⭐⭐⭐(中等)
开发难度⭐(很简单)⭐⭐⭐⭐⭐(很难)⭐⭐⭐(中等)
适用数据库仅关系型全部全部
锁定资源短时间短时间不锁定
适用场景常规微服务金融核心长流程

🎮 选型决策树

graph TD
    A[开始选型] --> B{使用NoSQL吗?}
    B -->|是| C{长流程?}
    B -->|否| D{能接受脏读吗?}
    C -->|是| E[选择Saga]
    C -->|否| F[选择TCC]
    D -->|是| G{追求开发效率?}
    D -->|否| H[选择TCC]
    G -->|是| I[选择AT]
    G -->|否| J[选择TCC]

🎯 推荐选择

场景推荐模式理由
电商下单AT流程短,用MySQL,追求开发效率
银行转账TCC必须强一致,不能脏读
订单审批流Saga长流程,涉及多个审批节点
积分系统AT允许最终一致,快速开发
库存扣减(Redis)TCC使用NoSQL
工单系统Saga长流程,多状态流转

💼 第五章:实际项目经验分享

案例1:电商项目的混合使用

@Service
public class OrderBusinessService {
    
    @Autowired
    private OrderService orderService;
    @Autowired
    private StockService stockService;    // MySQL,使用AT
    @Autowired
    private AccountService accountService; // MySQL,使用AT
    @Autowired
    private RedisStockService redisStock;  // Redis,使用TCC
    
    @GlobalTransactional
    public void createOrder(OrderDTO dto) {
        // 1. 创建订单(AT模式,自动补偿)
        orderService.create(dto);
        
        // 2. 扣减MySQL库存(AT模式)
        stockService.deduct(dto.getProductId(), dto.getQuantity());
        
        // 3. 扣减Redis缓存库存(TCC模式)
        redisStock.tryFreeze(dto.getProductId(), dto.getQuantity());
        
        // 4. 扣减账户余额(AT模式)
        accountService.deduct(dto.getUserId(), dto.getAmount());
        
        // 混合使用AT和TCC,发挥各自优势!
    }
}

案例2:性能优化

// 优化前:全部使用AT模式
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    orderService.create(dto);      // 需要锁行
    stockService.deduct(dto);      // 需要锁行
    accountService.deduct(dto);    // 需要锁行
    pointsService.add(dto);        // 需要锁行
    // 全部在一个分布式事务中,性能差
}

// 优化后:核心用AT,非核心用消息
@GlobalTransactional
public void createOrder(OrderDTO dto) {
    // 核心流程:订单、库存、支付(AT模式)
    orderService.create(dto);
    stockService.deduct(dto);
    accountService.deduct(dto);
    
    // 非核心流程:异步处理(消息队列)
    rabbitTemplate.send("points.add", dto); // 积分
    rabbitTemplate.send("coupon.send", dto); // 优惠券
    rabbitTemplate.send("notify.send", dto); // 通知
}

🎓 第六章:面试高分回答

问题:Seata的AT、TCC、Saga怎么选?

标准回答

"这三种模式各有优劣,需要根据实际场景选择:

AT模式是我们项目中用得最多的,因为:

  1. 业务代码几乎不用改,只需加@GlobalTransactional
  2. Seata自动生成回滚日志,自动补偿
  3. 性能也不错,因为第二阶段是异步的

但AT模式只支持关系型数据库,而且有脏读风险。

TCC模式我们在Redis库存扣减时使用,因为:

  1. AT不支持NoSQL
  2. TCC可以自己控制冻结逻辑
  3. 性能最好,无脏读问题

缺点是开发成本高,需要写Try、Confirm、Cancel,还要处理空回滚、幂等、悬挂。

Saga模式适合长流程,比如工单审批系统:

  1. 流程可能跨越几天
  2. 不能长时间锁定资源
  3. 通过状态机管理流程

总结:常规场景用AT,NoSQL用TCC,长流程用Saga。"

常见追问

Q1:AT模式的脏读怎么解决?

A:有几种方案:
1. @GlobalLock + SELECT FOR UPDATE:在查询时加全局锁
2. 业务设计上避免:比如库存扣减后立即释放,其他事务读到的就是准确的
3. 使用TCC模式替代

Q2:TCC的空回滚、幂等、悬挂具体怎么实现?

A:通过中间状态表:
1. 空回滚:Cancel时检查Try是否执行过,没执行就插入CANCELLED状态
2. 幂等:Try/Confirm/Cancel都先检查businessKey是否已处理过
3. 悬挂:Try时检查是否已经Cancel过,如果是就拒绝执行

🎁 总结:一句话记住

  • AT模式:自动挡,省心但只能走平路(MySQL)
  • TCC模式:手动挡,累但能走各种路(NoSQL)
  • Saga模式:长途客车,适合长途旅行(长流程)

📚 扩展资源


记住:没有最好的模式,只有最合适的选择!🎯

祝你面试顺利!💪✨