分布式事务核心:从2PC到TCC的实战之路

大家好,我是你们的老朋友。今天我们来聊聊分布式事务这块硬骨头。

说实话,分布式事务是很多团队绕不过去的坎。我见过不少项目,前期图省事不用分布式事务,结果数据不一致了,半夜爬起来修数据,那叫一个酸爽。所以,咱们今天就把2PC、3PC、TCC这三个核心方案彻底讲透。

一、2PC(两阶段提交):最经典的分布式事务协议

2PC,全称Two-Phase Commit。说白了,就是一个协调者带着一群参与者,分两步走完一个事务。

1.1 第一阶段:投票阶段

协调者问所有参与者:“我要提交了,你们准备好了吗?”

  • 参与者各自执行本地事务,但先不提交
  • 写undo/redo日志,保证能回滚
  • 返回Yes或No给协调者

1.2 第二阶段:提交/回滚阶段

协调者根据投票结果做决策:

  • 如果所有人都说Yes,那就广播Commit
  • 只要有一个人说No,那就广播Rollback

核心要点:2PC是强一致性协议,要么全部成功,要么全部失败。但它的阻塞问题很要命——参与者一直持有资源锁,直到收到协调者的最终指令。

我的经验:我在做金融交易系统时用过2PC。当时有个场景,协调者挂了,参与者一直等Commit指令,锁了半小时。从那以后,我建议所有用2PC的团队,必须给协调者做高可用,否则就是给自己埋雷。

1.3 2PC的代码骨架

// 协调者伪代码
public class Coordinator {
    public boolean executeTransaction() {
        // 第一阶段:投票
        for (Participant p : participants) {
            boolean ready = p.prepare();
            if (!ready) {
                // 有人投No,回滚
                for (Participant p2 : participants) {
                    p2.rollback();
                }
                return false;
            }
        }
        // 第二阶段:提交
        for (Participant p : participants) {
            p.commit();
        }
        return true;
    }
}

注意:2PC在第二阶段如果网络分区,参与者可能永远收不到Commit指令。这就是所谓的“悬挂事务”。我曾经遇到过,最后只能人工介入,手动查日志确认状态。

二、3PC(三阶段提交):2PC的改进版

3PC把2PC的两阶段拆成了三阶段,核心思想是引入超时机制,减少阻塞时间。

2.1 三个阶段详解

阶段 动作 说明
CanCommit 询问能否提交 只是问问,不执行任何操作
PreCommit 预提交 执行事务,写日志,但不提交
DoCommit 最终提交 提交或回滚

你想想看,3PC比2PC多了一个CanCommit阶段。这个阶段的好处是:如果某个参与者明显不行,那就不用浪费资源去执行PreCommit了。

关键改进:3PC引入了超时机制。参与者在PreCommit阶段如果超时没收到DoCommit指令,会自动提交。这解决了2PC的阻塞问题,但也带来了数据不一致的风险——万一协调者想回滚,但参与者已经自动提交了呢?

2.2 3PC的优缺点

  • 优点:减少了阻塞时间,参与者可以超时自决
  • 缺点:引入了新的不一致场景,实现复杂度更高

我的建议:3PC在实际项目中用得不多。我个人更倾向于用TCC或者Saga。3PC的理论价值大于实践价值,但理解它能帮你更好地理解分布式事务的演进思路。

三、TCC(Try-Confirm-Cancel):实战之王

TCC是现在最流行的分布式事务方案。它把每个服务的事务操作拆成三步:Try、Confirm、Cancel。

3.1 TCC的三个阶段

  • Try:预留资源。比如扣库存时,先冻结库存,不是直接扣减
  • Confirm:确认执行。把预留的资源真正扣掉
  • Cancel:取消操作。释放预留的资源

核心思想:TCC是业务层面的分布式事务,不是数据库层面的。它要求每个服务自己实现Try、Confirm、Cancel三个接口。说白了,就是把分布式事务的控制权交给业务代码。

3.2 TCC实战代码示例

// 库存服务的TCC实现
public class InventoryService {
    
    // Try阶段:冻结库存
    public boolean tryDeduct(Long orderId, Long productId, int quantity) {
        // 检查库存是否充足
        int available = getAvailableStock(productId);
        if (available < quantity) {
            return false; // 库存不足,Try失败
        }
        // 冻结库存
        freezeStock(productId, quantity);
        // 记录Try日志,用于后续Confirm或Cancel
        saveTryLog(orderId, productId, quantity);
        return true;
    }
    
    // Confirm阶段:真正扣减库存
    public boolean confirmDeduct(Long orderId) {
        TryLog log = getTryLog(orderId);
        if (log == null) return false;
        // 从冻结库存中扣减
        deductFrozenStock(log.productId, log.quantity);
        // 删除Try日志
        deleteTryLog(orderId);
        return true;
    }
    
    // Cancel阶段:释放冻结库存
    public boolean cancelDeduct(Long orderId) {
        TryLog log = getTryLog(orderId);
        if (log == null) return false;
        // 释放冻结库存
        unfreezeStock(log.productId, log.quantity);
        // 删除Try日志
        deleteTryLog(orderId);
        return true;
    }
}

避坑指南:我曾经在TCC的Cancel阶段踩过坑。当时Cancel接口幂等没做好,重复调用了两次,结果库存被释放了两次,变成了负数。记住:TCC的所有接口都必须幂等!

3.3 TCC的适用场景

场景 推荐方案 原因
短事务,高并发 TCC 资源锁持有时间短,性能好
长事务,低并发 2PC 强一致性,但锁时间长
最终一致性可接受 Saga 无锁,但需要补偿机制

四、三种方案对比总结

嗯,到这里,三种方案的核心内容都讲完了。我画了一张图,帮你快速理清思路:

分布式事务核心方案对比 2PC 两阶段提交 • 强一致性 • 阻塞问题 • 协调者单点 • 适合短事务 缺点:锁资源时间长 3PC 三阶段提交 • 减少阻塞 • 超时自决 • 实现复杂 • 理论价值高 缺点:数据不一致风险 TCC Try-Confirm-Cancel • 业务层面 • 资源预留 • 高性能 • 需幂等设计 缺点:业务侵入性强 总结:2PC保强一致,3PC减阻塞,TCC高性能。选型看场景,没有银弹。

最后说一句:分布式事务没有银弹。我见过太多团队盲目追求“完美方案”,结果把系统搞得很复杂。我的建议是:能不用分布式事务就不用,实在要用,优先考虑TCC。如果业务允许最终一致性,Saga也是好选择。

好了,这一章的内容就到这里。记住:理解原理比背代码更重要。下次遇到分布式事务问题,先想清楚你的业务场景,再选方案。


可可精品资料库