分布式事务与一致性模式:CAP理论、BASE理论、TCC模式、Saga模式、最终一致性、Seata实践

聊到分布式系统,有个话题永远绕不开——分布式事务

我记得刚带团队做微服务改造那会儿,大家最头疼的就是「数据不一致」。订单系统拆成了订单、库存、支付三个服务,结果用户下单后,库存扣了,支付却失败了。你说这账怎么算?

嗯,今天我们就来系统梳理一下,分布式事务到底该怎么玩。

一、CAP理论:分布式系统的「不可能三角」

先说说CAP。这个理论我每次面试都会问,但真正理解透的人不多。

CAP说的是:一个分布式系统,最多只能同时满足一致性(Consistency)可用性(Availability)分区容错性(Partition Tolerance)中的两个。

  • C(一致性):所有节点在同一时刻看到的数据是一样的。
  • A(可用性):每个请求都能收到一个响应,不管成功还是失败。
  • P(分区容错性):系统内部出现网络分区,依然能正常工作。

核心结论:在分布式环境下,P是必须选的。你只能在CP和AP之间做取舍。

我在项目中遇到过这样的场景:一个金融交易系统,我们选择了CP(强一致性),结果网络抖动时整个服务不可用。后来改成了AP(最终一致性),用户体验好了很多,但需要额外处理数据对账。

我的建议:业务场景决定取舍。金融、支付类选CP;社交、内容类选AP。别死磕理论,要结合实际。

二、BASE理论:AP的「妥协艺术」

CAP说不能同时满足三者,那怎么办?BASE理论给出了答案。

BASE是Basically Available(基本可用)Soft State(软状态)Eventually Consistent(最终一致性)的缩写。

  • 基本可用:系统允许部分功能降级,比如双十一时把「查看历史订单」改成只查最近三个月。
  • 软状态:允许数据中间状态存在,不要求实时一致。
  • 最终一致性:经过一段时间后,数据最终会达成一致。

说白了,BASE就是「别那么较真,等一会儿就好了」。你想想看,你在淘宝下单后,订单状态从「待支付」变成「已支付」可能需要几秒钟,这就是最终一致性。

注意:最终一致性不是「不管了」,而是「有保障的延迟一致」。你需要设计补偿机制,比如定时对账、消息重试。

三、TCC模式:两阶段提交的「改良版」

TCC(Try-Confirm-Cancel)是我个人比较喜欢的一种模式。它把事务拆成三个阶段:

  1. Try:预留资源。比如扣库存时,先冻结库存,不实际扣减。
  2. Confirm:确认执行。如果所有Try都成功,就执行Confirm,真正扣减库存。
  3. Cancel:取消回滚。如果某个Try失败,就执行Cancel,释放预留的资源。

我曾经在一个电商项目中用过TCC。当时库存服务需要预留库存,支付服务需要冻结金额。如果支付失败,库存的Cancel会释放预留库存。整个过程看起来挺完美,但有个坑——空回滚幂等性

避坑指南:我曾经因为没处理好空回滚,导致Cancel操作把未预留的库存释放了,结果库存变成负数。解决方案是:每个Try操作都生成一个全局事务ID,Cancel时先检查该ID是否存在。

代码示例(伪代码):

// Try阶段
public boolean try(TransactionContext ctx, Long orderId, Long productId, int quantity) {
    // 生成事务记录
    transactionRecordDao.insert(ctx.getTxId(), orderId, productId, quantity, "TRY");
    // 冻结库存
    return inventoryService.freezeStock(productId, quantity);
}

// Confirm阶段
public boolean confirm(TransactionContext ctx) {
    // 检查状态
    if (transactionRecordDao.getStatus(ctx.getTxId()) != "TRY") {
        return false; // 幂等性检查
    }
    // 实际扣减库存
    inventoryService.deductStock(ctx.getProductId(), ctx.getQuantity());
    transactionRecordDao.updateStatus(ctx.getTxId(), "CONFIRMED");
    return true;
}

// Cancel阶段
public boolean cancel(TransactionContext ctx) {
    if (transactionRecordDao.getStatus(ctx.getTxId()) == "CANCELED") {
        return true; // 防止重复取消
    }
    inventoryService.unfreezeStock(ctx.getProductId(), ctx.getQuantity());
    transactionRecordDao.updateStatus(ctx.getTxId(), "CANCELED");
    return true;
}

四、Saga模式:长事务的「分步走」

Saga模式跟TCC不同,它不要求预留资源,而是把一个大事务拆成多个本地事务,每个本地事务都有对应的补偿事务

举个例子:一个旅游预订系统,需要依次预订机票、酒店、租车。如果租车失败,就需要取消机票和酒店的预订。

Saga有两种实现方式:

  • 编排式(Choreography):每个服务监听事件,自己决定下一步。适合简单场景。
  • 协调式(Orchestration):一个协调器统一调度。适合复杂场景。

我的经验:项目初期用编排式,代码很简洁。但业务复杂后,事件满天飞,调试起来想哭。后来我改成了协调式,虽然多写了一个协调器,但可维护性好了很多。

协调式Saga的代码骨架:

public class BookingSagaOrchestrator {
    
    public void execute(BookingRequest request) {
        try {
            // 步骤1:预订机票
            String flightId = flightService.bookFlight(request);
            // 步骤2:预订酒店
            String hotelId = hotelService.bookHotel(request);
            // 步骤3:预订租车
            String carId = carService.bookCar(request);
            // 全部成功
            sagaState = SagaState.COMPLETED;
        } catch (Exception e) {
            // 执行补偿
            compensate(request, flightId, hotelId, carId);
        }
    }
    
    private void compensate(BookingRequest request, String flightId, String hotelId, String carId) {
        // 逆序取消
        if (carId != null) carService.cancelBooking(carId);
        if (hotelId != null) hotelService.cancelBooking(hotelId);
        if (flightId != null) flightService.cancelBooking(flightId);
        sagaState = SagaState.COMPENSATED;
    }
}

五、最终一致性:消息驱动的「异步方案」

最终一致性是我用得最多的方案。核心思想是:通过消息队列,保证数据最终一致

流程很简单:

  1. 主服务执行本地事务,同时发送一条消息到MQ。
  2. 从服务消费消息,执行自己的本地事务。
  3. 如果从服务失败,MQ会重试,直到成功。

这里有个关键点:本地事务和消息发送必须保证原子性。我见过很多团队直接用「先发消息,再执行本地事务」,结果本地事务失败,消息已经发出去了。

解决方案:使用「本地消息表」或「事务消息」。RocketMQ的事务消息就是为此设计的。

RocketMQ事务消息示例:

// 发送半消息
TransactionMQProducer producer = new TransactionMQProducer("order_group");
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // 执行本地事务(创建订单)
        boolean success = orderService.createOrder((Order) arg);
        return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
    }
    
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 回查本地事务状态
        String orderId = msg.getKeys();
        return orderService.isOrderCreated(orderId) ? 
               LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
    }
});
producer.sendMessageInTransaction(msg, order);

六、Seata实践:开箱即用的分布式事务框架

说了这么多理论,来点实战的。Seata是阿里开源的分布式事务框架,支持AT、TCC、Saga、XA四种模式。

我个人最常用的是AT模式。它基于两阶段提交,但对业务代码侵入很小。你只需要在业务方法上加个注解:

@GlobalTransactional(name = "create-order", timeoutMills = 30000)
public void createOrder(Order order) {
    // 1. 创建订单
    orderDao.insert(order);
    // 2. 扣减库存
    inventoryService.deduct(order.getProductId(), order.getQuantity());
    // 3. 扣减余额
    accountService.debit(order.getUserId(), order.getAmount());
}

Seata会自动拦截这个方法的SQL,生成前置镜像后置镜像。如果后续步骤失败,Seata会根据镜像数据自动回滚。

注意:AT模式依赖数据库的本地事务,且要求SQL必须是可回滚的。如果你用了MyBatis的批量更新,或者调用了第三方API,AT模式就搞不定了。这时候可以考虑TCC或Saga。

Seata的部署架构也很简单:

  • TC(事务协调器):独立部署的服务,维护全局事务状态。
  • TM(事务管理器):嵌入在业务应用中,负责开启、提交、回滚全局事务。
  • RM(资源管理器):管理分支事务,与TC通信。

七、知识体系总览

下面这张图,是我梳理的分布式事务知识体系。你可以把它当作一个「决策地图」:

分布式事务与一致性模式知识体系 分布式事务 CAP理论 BASE理论 最终一致性 TCC模式 Saga模式 消息驱动 Try-Confirm-Cancel 编排/协调 本地消息表 Seata(AT/TCC/Saga) RocketMQ事务消息 选择依据:业务一致性要求 > 系统可用性要求 > 开发维护成本

八、总结与选择建议

说了这么多,到底该怎么选?我根据自己的经验,给个参考:

场景 推荐模式 理由
金融交易、支付 TCC 或 Seata AT 强一致性要求,但需要高可用
订单、库存 Saga 或 最终一致性 允许短暂不一致,补偿逻辑清晰
跨系统数据同步 消息驱动 + 最终一致性 解耦性好,扩展性强
简单场景、小团队 Seata AT 侵入性低,上手快

最后说一句:没有银弹。分布式事务的本质是「用复杂度换一致性」。如果你的业务可以用单机事务解决,就别碰分布式事务。我见过太多团队为了「微服务而微服务」,结果引入了一堆分布式事务问题。

嗯,记住一句话:能不拆,就不拆;非要拆,先想好怎么补偿


公众号:蓝海资料掘金营,微信deep3321