💍 从相亲到结婚的漫长征程:分布式事务的四种解决方案

22 阅读12分钟

面试官:分布式事务怎么解决?
候选人:用2PC啊!
面试官:2PC有什么问题?TCC、Saga了解吗?
候选人:😰💦(内心OS:完了,只知道个名字...)

别慌!今天我们用最生动的方式,把分布式事务的四大门派讲得明明白白!


🎬 开篇:为什么需要分布式事务?

单体应用时代(一个人说了算)

下单 → 减库存 → 扣款 → 增积分
     ↓
  一个数据库,一个事务,简单!
  @Transactional 搞定! ✅

微服务时代(众人拾柴火焰高,但也容易扯皮)

订单服务(DB1) → 创建订单
库存服务(DB2) → 减库存  
支付服务(DB3) → 扣款
积分服务(DB4) → 加积分

问题:如果扣款成功,但加积分失败了怎么办?😱

核心痛点:不同服务使用不同数据库,无法用传统的ACID事务保证一致性!


🏛️ 第一章:2PC(两阶段提交)- 严肃的包办婚姻

原理:分成"准备"和"提交"两个阶段

                 协调者(婚姻介绍所)
                      │
        ┌─────────────┼─────────────┐
        │             │             │
      服务A         服务B         服务C
    (新郎家)      (新娘家)      (酒店)

🎭 生活比喻:包办婚姻的流程

阶段一:准备阶段(Prepare - 订婚)

婚姻介绍所:各位,咱们准备办婚礼了,你们准备好了吗?

新郎家:✅ 彩礼准备好了,可以!
新娘家:✅ 嫁妆准备好了,可以!  
酒店:✅ 婚宴场地订好了,可以!

→ 所有人都准备好了,但还没真正行动

阶段二:提交阶段(Commit - 正式结婚)

婚姻介绍所:好!大家都准备好了,正式开始吧!

新郎家:💰 交彩礼
新娘家:📦 送嫁妆
酒店:🍽️ 开席

→ 所有人同时行动,婚礼成功!✨

💻 代码示例

// 协调者
@Service
public class TransactionCoordinator {
    
    @Autowired
    private OrderService orderService;
    @Autowired
    private StockService stockService;
    @Autowired
    private PaymentService paymentService;
    
    public void execute() {
        String txId = UUID.randomUUID().toString();
        
        try {
            // 阶段一:准备阶段(向所有参与者发送prepare请求)
            boolean orderPrepared = orderService.prepare(txId);
            boolean stockPrepared = stockService.prepare(txId);
            boolean paymentPrepared = paymentService.prepare(txId);
            
            // 如果所有服务都准备好了
            if (orderPrepared && stockPrepared && paymentPrepared) {
                // 阶段二:提交阶段
                orderService.commit(txId);
                stockService.commit(txId);
                paymentService.commit(txId);
                System.out.println("事务提交成功!✅");
            } else {
                // 有任何一个服务准备失败,全部回滚
                orderService.rollback(txId);
                stockService.rollback(txId);
                paymentService.rollback(txId);
                System.out.println("事务回滚!❌");
            }
        } catch (Exception e) {
            // 异常时全部回滚
            orderService.rollback(txId);
            stockService.rollback(txId);
            paymentService.rollback(txId);
        }
    }
}

// 参与者
@Service
public class OrderService {
    
    private Map<String, Order> preparedOrders = new ConcurrentHashMap<>();
    
    public boolean prepare(String txId) {
        try {
            // 准备创建订单,但不真正提交到数据库
            Order order = new Order();
            preparedOrders.put(txId, order);
            return true; // 准备成功
        } catch (Exception e) {
            return false; // 准备失败
        }
    }
    
    public void commit(String txId) {
        Order order = preparedOrders.get(txId);
        orderRepository.save(order); // 真正提交
        preparedOrders.remove(txId);
    }
    
    public void rollback(String txId) {
        preparedOrders.remove(txId); // 清除准备的数据
    }
}

⚖️ 优缺点

✅ 优点

  • 强一致性:要么全成功,要么全失败
  • 实现相对简单:逻辑清晰,容易理解

❌ 缺点

  1. 同步阻塞:准备阶段所有参与者都要等待,性能差
  2. 单点故障:协调者挂了,整个系统瘫痪
  3. 数据不一致风险:如果第二阶段网络分区,部分提交部分未提交
  4. 资源锁定时间长:prepare到commit期间,资源一直被锁定
性能评分:⭐⭐ (2/5)
一致性评分:⭐⭐⭐⭐ (4/5)  
适用场景:低并发、强一致性要求

🎯 第二章:3PC(三阶段提交)- 改进版包办婚姻

原理:在2PC基础上增加"预询问"阶段,引入超时机制

阶段1:CanCommit (能不能办?)
阶段2:PreCommit (准备办!)
阶段3:DoCommit (正式办!)

🎭 生活比喻:更谨慎的婚礼筹备

阶段一:CanCommit(预询问)

婚姻介绍所:各位,咱们打算办婚礼,你们有没有档期?

新郎家:有档期,可以考虑 ✅
新娘家:有档期,可以考虑 ✅
酒店:有档期,可以考虑 ✅

→ 只是询问意向,不锁定资源

阶段二:PreCommit(预提交)

婚姻介绍所:好,那大家开始准备吧!

新郎家:✅ 彩礼准备好了,锁定!
新娘家:✅ 嫁妆准备好了,锁定!
酒店:✅ 场地预定了,锁定!

→ 锁定资源,但还没真正执行

阶段三:DoCommit(提交)

婚姻介绍所:正式开始!

(后续流程与2PC的commit阶段相同)

⚖️ 相比2PC的改进

  1. 增加了预询问阶段:减少不必要的资源锁定
  2. 引入超时机制
    • 协调者超时:参与者自动提交(optimistic)
    • 参与者超时:自动中断事务

❌ 但仍然存在的问题

  • 复杂度增加:多了一个阶段
  • 网络分区时仍可能不一致:超时自动提交可能导致问题
性能评分:⭐⭐⭐ (3/5)
一致性评分:⭐⭐⭐ (3/5)
适用场景:中等并发、相对较强一致性要求

结论:实际应用很少,因为更复杂但问题没完全解决!

💪 第三章:TCC(Try-Confirm-Cancel)- 自由恋爱模式

原理:业务层面的补偿型事务

  • Try:尝试执行,预留资源
  • Confirm:确认执行,使用预留资源
  • Cancel:取消执行,释放预留资源

🎭 生活比喻:租房的故事

Try阶段(看房预定)

你:老板,这房子我想租,先给我留着!
房东:好的,给你保留3天,但要交500元定金
银行:冻结你的账户里500元(不扣,只是冻结)

→ 预留资源,但没真正扣款

Confirm阶段(正式租房)

你:好,我决定租了!
房东:好的,合同签了,房子给你!
银行:把之前冻结的500元扣掉,转给房东

→ 真正执行业务,扣除预留资源

Cancel阶段(不租了)

你:不好意思,我不租了
房东:好吧,房子重新出租
银行:解冻你的500元,归还给你

→ 释放预留资源,回滚

💻 代码示例

// TCC订单服务
@Service
public class OrderTccService {
    
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private AccountTccService accountService;
    @Autowired
    private StockTccService stockService;
    
    /**
     * Try阶段:创建订单,冻结库存和余额
     */
    @Transactional
    public boolean tryCreateOrder(OrderDTO dto) {
        try {
            // 1. 创建订单,状态为"进行中"
            Order order = new Order();
            order.setStatus(OrderStatus.TRY);
            order.setAmount(dto.getAmount());
            orderRepository.save(order);
            
            // 2. 冻结库存(不减库存,只是标记冻结)
            boolean stockFrozen = stockService.tryFreeze(
                dto.getProductId(), 
                dto.getQuantity(),
                order.getId()
            );
            
            // 3. 冻结账户余额(不扣款,只是冻结)
            boolean balanceFrozen = accountService.tryFreeze(
                dto.getUserId(),
                dto.getAmount(),
                order.getId()
            );
            
            return stockFrozen && balanceFrozen;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * Confirm阶段:确认订单,真正扣减库存和余额
     */
    @Transactional
    public void confirmCreateOrder(String orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        
        // 1. 更新订单状态为"成功"
        order.setStatus(OrderStatus.SUCCESS);
        orderRepository.save(order);
        
        // 2. 真正扣减库存(将冻结的库存扣掉)
        stockService.confirm(orderId);
        
        // 3. 真正扣款(将冻结的余额扣掉)
        accountService.confirm(orderId);
    }
    
    /**
     * Cancel阶段:取消订单,释放冻结的资源
     */
    @Transactional
    public void cancelCreateOrder(String orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        
        // 1. 更新订单状态为"取消"
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        
        // 2. 释放冻结的库存
        stockService.cancel(orderId);
        
        // 3. 释放冻结的余额
        accountService.cancel(orderId);
    }
}

// 账户TCC服务
@Service
public class AccountTccService {
    
    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private FreezeRecordRepository freezeRecordRepository;
    
    /**
     * Try:冻结金额
     */
    @Transactional
    public boolean tryFreeze(String userId, BigDecimal amount, String orderId) {
        Account account = accountRepository.findByUserId(userId);
        
        // 检查余额是否足够
        if (account.getBalance().compareTo(amount) < 0) {
            return false;
        }
        
        // 增加冻结金额
        account.setFrozenAmount(account.getFrozenAmount().add(amount));
        accountRepository.save(account);
        
        // 记录冻结记录(用于Confirm和Cancel)
        FreezeRecord record = new FreezeRecord();
        record.setOrderId(orderId);
        record.setUserId(userId);
        record.setAmount(amount);
        record.setStatus("FROZEN");
        freezeRecordRepository.save(record);
        
        return true;
    }
    
    /**
     * Confirm:真正扣款
     */
    @Transactional
    public void confirm(String orderId) {
        FreezeRecord record = freezeRecordRepository.findByOrderId(orderId);
        Account account = accountRepository.findByUserId(record.getUserId());
        
        // 减少余额
        account.setBalance(account.getBalance().subtract(record.getAmount()));
        // 减少冻结金额
        account.setFrozenAmount(account.getFrozenAmount().subtract(record.getAmount()));
        accountRepository.save(account);
        
        // 更新记录状态
        record.setStatus("CONFIRMED");
        freezeRecordRepository.save(record);
    }
    
    /**
     * Cancel:解冻金额
     */
    @Transactional
    public void cancel(String orderId) {
        FreezeRecord record = freezeRecordRepository.findByOrderId(orderId);
        Account account = accountRepository.findByUserId(record.getUserId());
        
        // 减少冻结金额(归还)
        account.setFrozenAmount(account.getFrozenAmount().subtract(record.getAmount()));
        accountRepository.save(account);
        
        // 更新记录状态
        record.setStatus("CANCELLED");
        freezeRecordRepository.save(record);
    }
}

⚖️ 优缺点

✅ 优点

  1. 性能较好:不长时间锁定资源
  2. 不依赖资源管理器:业务层面实现,灵活性高
  3. 强一致性:通过补偿机制保证

❌ 缺点

  1. 业务侵入性强:需要为每个操作实现Try、Confirm、Cancel
  2. 开发成本高:要处理很多边界情况
  3. 数据库设计复杂:需要冻结字段、状态字段等
性能评分:⭐⭐⭐⭐ (4/5)
一致性评分:⭐⭐⭐⭐ (4/5)
开发难度:⭐⭐⭐⭐⭐ (5/5 - 很难!)

适用场景:金融支付、核心交易系统
典型应用:阿里巴巴Seata的TCC模式

🌊 第四章:Saga模式 - 长征式的分步提交

原理:将分布式事务拆分成多个本地事务,每个事务都有对应的补偿操作

正向流程:T1 → T2 → T3 → T4 → 成功!✅

失败回滚:T1 → T2 → T3失败 → C3 → C2 → C1 → 回滚完成
         (做) (做) (失败)  (补偿)(补偿)(补偿)

🎭 生活比喻:自助旅行

正向流程(一切顺利)

第1步:订机票 ✅
第2步:订酒店 ✅
第3步:订景点门票 ✅
第4步:订餐厅 ✅

→ 旅行愉快!🎉

补偿流程(中途出问题)

第1步:订机票 ✅
第2步:订酒店 ✅
第3步:订景点门票 ❌(门票卖完了!)

开始补偿:
第2步补偿:取消酒店预订 ✅
第1步补偿:退机票 ✅

→ 旅行取消,全部退款

💻 代码示例(基于消息队列)

// Saga协调器
@Service
public class OrderSagaOrchestrator {
    
    @Autowired
    private OrderService orderService;
    @Autowired
    private StockService stockService;
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private PointsService pointsService;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 编排式Saga:由协调器控制流程
     */
    public void executeOrderSaga(OrderDTO dto) {
        String sagaId = UUID.randomUUID().toString();
        
        try {
            // Step 1: 创建订单
            String orderId = orderService.createOrder(dto, sagaId);
            
            // Step 2: 扣减库存
            stockService.deductStock(dto.getProductId(), dto.getQuantity(), sagaId);
            
            // Step 3: 扣款
            paymentService.pay(dto.getUserId(), dto.getAmount(), sagaId);
            
            // Step 4: 增加积分
            pointsService.addPoints(dto.getUserId(), dto.getPoints(), sagaId);
            
            // 全部成功
            log.info("Saga事务成功!sagaId: {}", sagaId);
            
        } catch (StockNotEnoughException e) {
            // Step 2失败,补偿Step 1
            log.error("库存不足,开始补偿...");
            orderService.cancelOrder(orderId, sagaId);
            
        } catch (PaymentFailedException e) {
            // Step 3失败,补偿Step 2和Step 1
            log.error("支付失败,开始补偿...");
            stockService.returnStock(dto.getProductId(), dto.getQuantity(), sagaId);
            orderService.cancelOrder(orderId, sagaId);
            
        } catch (PointsException e) {
            // Step 4失败,补偿Step 3、Step 2、Step 1
            log.error("积分增加失败,开始补偿...");
            paymentService.refund(dto.getUserId(), dto.getAmount(), sagaId);
            stockService.returnStock(dto.getProductId(), dto.getQuantity(), sagaId);
            orderService.cancelOrder(orderId, sagaId);
        }
    }
}

// 订单服务
@Service
public class OrderService {
    
    /**
     * 正向操作:创建订单
     */
    @Transactional
    public String createOrder(OrderDTO dto, String sagaId) {
        Order order = new Order();
        order.setSagaId(sagaId);
        order.setStatus(OrderStatus.CREATED);
        order.setAmount(dto.getAmount());
        orderRepository.save(order);
        
        log.info("订单创建成功:{}", order.getId());
        return order.getId();
    }
    
    /**
     * 补偿操作:取消订单
     */
    @Transactional
    public void cancelOrder(String orderId, String sagaId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        
        log.info("订单已取消(补偿):{}", orderId);
    }
}

📊 Saga的两种实现方式

1️⃣ 编排式Saga (Orchestration)

              Saga协调器
                 │
     ┌───────────┼───────────┐
     ↓           ↓           ↓
  订单服务    库存服务    支付服务

协调器控制全流程,类似导演指挥演员

优点:流程集中管理,容易理解和监控
缺点:协调器是单点,耦合度高

2️⃣ 编舞式Saga (Choreography)

订单服务 → (发消息) → 库存服务 → (发消息) → 支付服务
   ↑                                           ↓
   └──────────── (失败消息) ─────────────────┘

每个服务监听消息,自主决定下一步,类似接力赛

优点:服务解耦,没有单点
缺点:流程分散,难以追踪和调试

⚖️ 优缺点

✅ 优点

  1. 适合长事务:可以跨越很长时间(几分钟甚至几小时)
  2. 业务侵入性低:相比TCC简单很多
  3. 性能好:不长时间锁定资源

❌ 缺点

  1. 只能保证最终一致性:中间状态可能被看到
  2. 补偿逻辑复杂:需要为每个操作写补偿
  3. 难以调试:分布式流程追踪困难
性能评分:⭐⭐⭐⭐⭐ (5/5)
一致性评分:⭐⭐⭐ (3/5 - 最终一致)
开发难度:⭐⭐⭐ (3/5)

适用场景:长流程、对一致性要求不那么严格的场景
典型应用:微服务架构、复杂业务流程

📊 第五章:四种方案全方位对比

维度2PC3PCTCCSaga
一致性强一致强一致强一致最终一致
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
复杂度简单复杂非常复杂中等
业务侵入
适用场景低并发很少用金融交易长流程
锁定资源长时间较长时间短时间不锁定
实现难度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

🎯 选型决策树

            开始选型
               │
        是否需要强一致性?
         ┌─────┴─────┐
       是│           │否
         │           │
    并发量高吗?    选择Saga
     ┌───┴───┐      (最终一致)
   高│       │低
     │       │
  选择TCC  选择2PC
 (强一致   (强一致
  高性能)   简单)

💼 第六章:实际项目选型建议

场景1:电商下单

/**
 * 推荐方案:Saga(编排式)
 * 
 * 理由:
 * 1. 流程长(订单→库存→支付→积分→通知)
 * 2. 允许最终一致性(用户可以接受积分延迟到账)
 * 3. 高并发场景
 */
@Service
public class OrderSagaService {
    
    public void createOrder(OrderDTO dto) {
        // 1. 创建订单(立即成功)
        Order order = orderService.create(dto);
        
        // 2. 发送消息到下游服务(异步)
        sagaOrchestrator.execute(order);
    }
}

场景2:银行转账

/**
 * 推荐方案:TCC
 * 
 * 理由:
 * 1. 强一致性要求(钱不能少也不能多)
 * 2. 流程短
 * 3. 可以接受较高开发成本
 */
@Service
public class TransferTccService {
    
    public void transfer(String from, String to, BigDecimal amount) {
        try {
            // Try:冻结转出账户金额
            accountService.tryFreeze(from, amount);
            // Try:预增加转入账户金额
            accountService.tryIncrease(to, amount);
            
            // Confirm:真正扣款和加款
            accountService.confirm(from, to, amount);
        } catch (Exception e) {
            // Cancel:回滚
            accountService.cancel(from, to, amount);
        }
    }
}

场景3:配置管理系统(小流量)

/**
 * 推荐方案:2PC
 * 
 * 理由:
 * 1. 并发量低
 * 2. 需要强一致性
 * 3. 实现简单,开发成本低
 */
@Service
public class ConfigSyncService {
    
    @GlobalTransactional // 使用Seata的2PC
    public void syncConfig(Config config) {
        configService.save(config);
        cacheService.update(config);
        auditService.log(config);
    }
}

🎓 第七章:面试高分回答模板

问题:你们项目中分布式事务怎么解决的?

标准回答(STAR法则):

S(背景):"我们是一个电商系统,下单流程涉及订单、库存、支付、积分四个微服务,每个服务独立数据库。"

T(任务):"需要保证这四个操作要么全成功,要么全失败,否则会出现扣款了但订单没创建的问题。"

A(行动):"我们采用了Saga模式:

  • 对于核心的订单创建和支付,使用Seata的TCC模式,保证强一致性
  • 对于非核心的积分增加、消息通知,使用消息队列实现最终一致性
  • 设计了补偿机制:支付失败自动取消订单,库存不足自动退款"

R(结果):"上线后,99.9%的订单能在3秒内完成,异常情况通过补偿机制在1分钟内恢复一致性,大促期间支撑了100万+订单。"

常见追问

Q1:TCC的空回滚和悬挂问题怎么处理?

A:空回滚:Cancel在Try之前到达
   → 解决:记录Try是否执行过,Cancel时检查

   悬挂:Try在Cancel之后到达
   → 解决:Cancel时记录状态,Try时检查Cancel是否已执行

Q2:Saga如何保证幂等性?

A:
1. 业务主键去重(订单号全局唯一)
2. 版本号机制(乐观锁)
3. 状态机(只允许特定状态转换)
4. 分布式锁(关键操作加锁)

🎁 总结:一句话记住四种方案

  • 2PC:严肃的包办婚姻(强一致但慢)
  • 3PC:改进版包办婚姻(多一个阶段,少用)
  • TCC:自由恋爱需要预定(预留资源,性能好,代码多)
  • Saga:长征式分步走(适合长流程,最终一致)

📚 实战框架推荐

  • Seata(阿里开源):支持AT、TCC、Saga、XA四种模式
  • ByteTCC:基于TCC的分布式事务框架
  • Hmily:高性能TCC框架
  • ServiceComb Pack:华为开源的Saga框架

记住:没有银弹,选择最适合业务场景的方案!🎯

面试加油!下一个offer就是你的!💪✨