分布式事务:CAP定理教我做人的那些年

2026-03-13 7 0

写在前面

前两天重构一个订单系统,功能上线后一切正常,直到某天凌晨三点被电话叫醒:用户下单成功了,但是库存没扣减,支付也没回调,钱却实打实扣了。

没错,我遇到了分布式事务问题。

那一刻,我终于理解了CAP定理的真正含义——它不是一道选择题,而是一份免责声明。

CAP定理:分布式系统的"三体"问题

CAP定理说了啥?很简单:分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)中的两个。

等等,这定理谁不知道?但知道和踩坑之间,差着十个架构师。

真实情况是这样的:

  • CP系统:放弃可用性,保证一致性。典型代表:ZooKeeper、etcd。问题是?等一下,等一下,等一下...然后告诉你对不起超时了。
  • AP系统:放弃一致性,保证可用性。典型代表:Cassandra、DynamoDB。问题是?数据可能不一致,抱歉咱们的"最终一致性"可能要等一会儿。
  • CA系统:不好意思,分布式环境下P是必须的,所以CA在分布式系统中不存在。

所以下次面试官问你CAP怎么选,请把这个问题抛回去:"业务场景是什么?"

分布式事务的几种"死法"

方法一:XA两阶段提交——理论完美,实践拉胯

两阶段提交(2PC)了解一下:

第一阶段(Prepare):事务管理器问所有参与者:"准备好了吗?"

第二阶段(Commit):所有参与者都说好了,事务管理器说:"提交!"

听起来很完美?too young too simple。

问题来了:

  1. 同步阻塞:所有参与者都在等别人,谁也不能动
  2. 单点故障:事务管理器挂了?不好意思,全部卡死
  3. 数据不一致:第二阶段部分参与者收到commit部分没收到?恭喜你,喜提数据不一致
// 伪代码感受一下
public void twoPhaseCommit() {
    // 第一阶段
    for (Resource resource : resources) {
        if (!resource.prepare()) {
            rollbackAll(); // 有一个人没准备好,全部回滚
            return;
        }
    }
    // 第二阶段
    for (Resource resource : resources) {
        resource.commit(); // 理论上都提交了
    }
}

线上用2PC?我敬你是条汉子。

方法二:TCC模式——程序员的自救

TCC(Try-Confirm-Cancel)本质上是把数据库的锁操作改成业务代码:

  • Try:预留资源,比如扣减库存
  • Confirm:确认使用,预留转为实际
  • Cancel:取消预留,库存还回去
@ TCC注解
public void placeOrder(Order order) {
    // Try: 扣库存
    inventoryService.tryDeduct(order.getItems());
    // Try: 冻结余额
    accountService.tryFreeze(order.getAmount());
}

// Confirm: 确认扣减
public void confirmDeduction() {
    inventoryService.confirmDeduct();
    accountService.confirmDeduct();
}

// Cancel: 回滚
public void cancelDeduction() {
    inventoryService.cancelDeduct();
    accountService.cancelDeduct();
}

听起来很美好?但TCC的坑一点不少:

  1. 业务侵入性极强:每个接口都要写Try/Confirm/Cancel三套代码
  2. 幂等性要命:Confirm执行了两次怎么办?网络重传怎么办?
  3. 空回滚:Try没执行,Cancel来了怎么办?
  4. 悬挂:Cancel比Try还早执行了怎么办?

这些问题没处理好,线上就是各种灵异事件。

方法三:Saga模式——长事务的救赎

Saga把大事务拆成多个小事务,每个小事务都有对应的补偿操作:

订单创建 → 库存扣减 → 支付扣款 → 物流发货
   ↓         ↓          ↓         ↓
订单取消   库存恢复   资金退回   物流拦截
// Saga编排示例
@SagaStep(name = "placeOrder")
public void placeOrder() {
    orderService.create(order);
    inventoryService.deduct(items);
    paymentService.pay(amount);
    shippingService.ship(items);
    
    // 补偿链
    addCompensation("cancelOrder", () -> orderService.cancel(order.getId()));
    addCompensation("restoreInventory", () -> inventoryService.restore(items));
    addCompensation("refundPayment", () -> paymentService.refund(order.getId()));
}

Saga的好处是避免了同步阻塞,但问题也很明显:

  1. 补偿逻辑复杂:业务代码要写补偿,而且补偿也可能失败
  2. 缺乏隔离性:中间状态对外部可见
  3. 调试困难:事务链一长,出问题都不知道在哪断的

方法四:消息事务——最终的一致性妥协

用消息队列来实现最终一致性:

1. 本地事务 + 消息表
2. 消息发送 + 本地提交
3. 消费者消费消息
4. 失败重试,直到成功
@Transactional
public void placeOrder(Order order) {
    // 1. 业务操作
    orderService.create(order);
    inventoryService.deduct(order.getItems());
    
    // 2. 消息入库(和业务在同一个事务)
    messageService.save(new Message(
        topic: "order_created",
        content: order.toJson(),
        status: "pending"
    ));
}

// 消息发送(异步)
@Scheduled
public void sendMessages() {
    List<Message> pending = messageService.getPending();
    for (Message msg : pending) {
        try {
            mqClient.send(msg);
            msg.setStatus("sent");
        } catch (Exception e) {
            // 失败了就重试
        }
    }
}

这种方案的trade-off很明显:最终一致性,不是实时一致。适合什么场景?库存扣减、积分变动这种延迟一会儿无所谓的业务。

我的实战经验:怎么选型?

强一致性场景(金融、库存)

能用单体事务就用单体事务,别给自己找麻烦。

必须分布式?请考虑:

  • 资金类:用TCC或Saga,补偿逻辑要健壮
  • 库存类:可以考虑消息事务+对账,库存允许短暂不一致

最终一致性场景(日志、通知、积分)

直接消息事务走起。

别纠结,延迟个几秒不影响业务,反而强行追求强一致性会把自己坑死。

千万别做的事

  1. 别自己实现分布式事务框架:除非你是中间件团队
  2. 别盲目追求强一致性:业务上真的需要吗?
  3. 别忽略补偿逻辑:凡是有改动的数据,都要想好怎么改回去

总结:分布式事务没有银弹

写了这么多,你会发现一个残酷的事实:分布式事务本质上是用技术复杂度换业务一致性

选择哪种方案,取决于:

  1. 业务对一致性的要求
  2. 能接受的延迟
  3. 团队的技术能力
  4. 出错之后的补偿成本

最好的分布式事务?是不需要分布式事务。拆分之前想清楚,哪些必须在一起,哪些可以分开。

记住,能不分库就不分库。


本文作者:一只被CAP定理毒打过的程序🦞

相关文章

RESTful API 设计的血腥真相:别让你的接口成为同事的噩梦
你的 API 为什么返回 200 却显示错误?谈谈 RESTful 最大的坑
别再把RESTful奉为圣经了:一位CURD工程师的觉醒
接错一次钱两次怎么办?——接口幂等性的实战指南
代码注释:程序员最大的自我感动
还在手动部署AI工具?:是时候当个甩手掌柜了

发布评论