🚀 分布式事务保姆级教程:从删库到跑路的那些坑,老司机带你一次踩个够!

26 阅读23分钟

作者:一个被分布式事务折磨过999次的10年老Java程序员
座右铭:能用分布式解决的问题,绝不用单体!能用补偿的问题,绝不用锁!💪


📚 目录


🎬 引子:那个让我秃头的需求 {#引子}

从前从前,在一个风和日丽的周一早晨...

产品经理小王兴冲冲地跑过来:"老李啊,咱们系统要重构成微服务了!"

我:"哦?为啥?"

小王:"因为老板说竞对都在用微服务,咱们也得用!而且听说微服务特别高大上!"

我(内心OS):完了,又是一个看了几篇技术博客就来指点江山的... 😓

结果一个月后,系统拆成了三个服务:

  • 📦 订单服务:负责创建订单
  • 🏪 库存服务:负责扣减库存
  • 💰 账户服务:负责扣款

然后问题就来了...

🔥 灾难现场

某天凌晨3点,我被一通电话吵醒:

运维小哥:"老李,出大事了!用户投诉说钱扣了,但是订单没创建成功!"

我一个激灵爬起来,打开监控一看:

订单创建:✅ 成功
库存扣减:✅ 成功  
账户扣款:✅ 成功

但是... 有20%的订单数据对不上!😱

这就是分布式事务的经典问题:在单体架构里,一个@Transactional注解就能解决的事儿,拆成微服务后,变成了世纪难题!


🎓 理论篇:先把原理整明白 {#理论篇}

什么是事务?用吃火锅来理解

想象你去吃火锅自助:

  1. 点菜(创建订单)
  2. 厨房拿食材(扣减库存)
  3. 结账(扣款)

在单体架构里,这就像你一个人承包了整个流程,要么全做完,要么一件不做。这就是ACID特性:

🎯 ACID特性(单体事务的四大金刚)

特性说明生活例子
Atomicity(原子性)要么全做,要么全不做要么火锅吃完结账,要么一口不吃不给钱
Consistency(一致性)数据前后保持一致你的钱包-100,商家的钱包+100
Isolation(隔离性)互不干扰你吃你的火锅,我吃我的,别抢我的肉!
Durability(持久性)做完就是做完了吃完了就是吃完了,不能反悔

分布式系统的噩梦:CAP定理

当你把系统拆成微服务,就像把火锅店拆成了三家店:

🏠 点菜店(订单服务)
🏠 食材店(库存服务)  
🏠 收银店(账户服务)

这时候,CAP定理告诉我们一个残酷的现实:鱼与熊掌不可兼得

📊 CAP定理图解

         C (一致性)
        /  \
       /    \
      /      \
     /        \
    /    😱    \
   /   只能选2  \
  /______________\
 A               P
(可用性)      (分区容错)
  • C (Consistency):所有节点看到的数据一致
  • A (Availability):系统随时可用,不宕机
  • P (Partition Tolerance):网络出问题了,系统还能撑住

重点来了:在分布式系统中,P是必选项(网络故障是常态),所以你只能在C和A之间二选一

BASE理论:退而求其次的智慧

既然CAP告诉我们"不可能三角",那咋办?聪明的架构师们提出了BASE理论:

  • BA (Basically Available):基本可用,系统有点慢没关系,但别挂掉
  • S (Soft state):软状态,可以有中间状态
  • E (Eventually consistent):最终一致性,暂时不一致没事,最后一致就行

🍜 用点外卖来理解BASE

1. 你下单了外卖(订单创建)
2. 商家接单了(库存扣减)
3. 支付成功了(账户扣款)

BUT... 外卖还在路上,还没到你手里!

这是"软状态",过程中暂时不一致
但最终,要么你吃到外卖,要么退款,达到"最终一致性"

⚔️ 方案篇:江湖上的六大门派 {#方案篇}

老司机告诉你,分布式事务的解决方案就像武侠小说里的各大门派,各有优劣,适用场景不同。

1️⃣ 两阶段提交(2PC)- 刚猛派 💪

原理图

        协调者(老大)
           |
    ___________________
   |        |          |
参与者1   参与者2   参与者3
(订单)    (库存)    (账户)

阶段一:准备阶段
老大:"兄弟们,准备好了吗?"
小弟1"我准备好了!"
小弟2"我也准备好了!"
小弟3"我也OK!"

阶段二:提交阶段
老大:"都准备好了,一起提交!"
所有小弟:"收到!提交完成!"

优点

  • ✅ 强一致性,严格的ACID保证
  • ✅ 逻辑简单,易于理解

缺点

  • 同步阻塞:所有小弟都得等老大的命令,干等着,浪费资源
  • 单点故障:老大挂了,所有小弟都傻眼
  • 数据不一致:网络问题可能导致部分提交成功,部分失败
  • 性能差:大家都在等,吞吐量低

生活例子

就像组团开黑打游戏:

  • 队长问:"大家准备好了吗?"
  • 所有人都要回:"准备好了!"
  • 队长:"好,一起冲!"

但如果队长突然掉线,所有人都不知道该不该冲... 😅

代码示例(伪代码)

// 协调者
public class TransactionCoordinator {
    
    public void execute2PC() {
        // 阶段一:准备
        boolean phase1 = prepare();
        
        // 阶段二:提交或回滚
        if (phase1) {
            commit();
        } else {
            rollback();
        }
    }
    
    private boolean prepare() {
        boolean orderOk = orderService.prepare();     // 订单准备
        boolean inventoryOk = inventoryService.prepare(); // 库存准备
        boolean accountOk = accountService.prepare();   // 账户准备
        
        return orderOk && inventoryOk && accountOk;
    }
    
    private void commit() {
        orderService.commit();
        inventoryService.commit();
        accountService.commit();
    }
    
    private void rollback() {
        orderService.rollback();
        inventoryService.rollback();
        accountService.rollback();
    }
}

2️⃣ 三阶段提交(3PC)- 改良派 🔧

2PC的弟弟,在2PC基础上加了个预备阶段,缓解了一些问题,但本质还是同步阻塞,现在基本没人用了。

我的评价:理论上很美好,实际上基本没用。跳过!⏭️

3️⃣ TCC模式 - 稳健派 🛡️

Try-Confirm-Cancel,这是我个人比较推荐的方案!

原理图

业务操作拆成三步:

Try阶段(尝试):
订单服务:冻结订单状态为"处理中"
库存服务:冻结库存(预扣)
账户服务:冻结金额(预扣款)

      ⬇️ 如果都成功

Confirm阶段(确认):
订单服务:订单状态改为"已完成"
库存服务:真正扣减库存
账户服务:真正扣款

      ⬇️ 如果任何一步失败

Cancel阶段(取消):
订单服务:删除订单
库存服务:释放冻结的库存
账户服务:释放冻结的金额

🍔 用麦当劳点餐理解TCC

Try阶段

  • 你:"我要一个巨无霸套餐"
  • 收银员:"稍等,我看看有没有..."(检查库存,冻结食材)
  • 你:"我看看钱包够不够..."(冻结余额)

Confirm阶段

  • 收银员:"有货!请付款!"
  • 你:"好的"(真正扣款)
  • 厨房:"开始制作"(真正扣减食材)

Cancel阶段

  • 收银员:"抱歉,汉堡肉刚好卖完了..."
  • 你:"那算了"(释放冻结的钱)
  • 厨房:"不用做了"(释放冻结的食材)

代码示例

// 订单服务
@Service
public class OrderTccService {
    
    // Try:尝试创建订单,订单状态为"处理中"
    @Tcc(confirmMethod = "confirmOrder", cancelMethod = "cancelOrder")
    public void tryCreateOrder(OrderRequest request) {
        Order order = new Order();
        order.setStatus("PROCESSING"); // 冻结状态
        order.setAmount(request.getAmount());
        orderRepository.save(order);
        
        log.info("Try阶段:订单已冻结,orderId={}", order.getId());
    }
    
    // Confirm:确认订单
    public void confirmOrder(OrderRequest request) {
        Order order = orderRepository.findById(request.getOrderId());
        order.setStatus("COMPLETED"); // 确认完成
        orderRepository.save(order);
        
        log.info("Confirm阶段:订单已确认,orderId={}", order.getId());
    }
    
    // Cancel:取消订单
    public void cancelOrder(OrderRequest request) {
        Order order = orderRepository.findById(request.getOrderId());
        order.setStatus("CANCELLED"); // 取消
        orderRepository.save(order);
        
        log.info("Cancel阶段:订单已取消,orderId={}", order.getId());
    }
}

// 库存服务
@Service
public class InventoryTccService {
    
    @Tcc(confirmMethod = "confirmReduce", cancelMethod = "cancelReduce")
    public void tryReduceInventory(String productId, int quantity) {
        // 冻结库存:可用库存-quantity,冻结库存+quantity
        Inventory inventory = inventoryRepository.findByProductId(productId);
        inventory.setAvailable(inventory.getAvailable() - quantity);
        inventory.setFrozen(inventory.getFrozen() + quantity);
        inventoryRepository.save(inventory);
        
        log.info("Try阶段:库存已冻结,productId={}, quantity={}", productId, quantity);
    }
    
    public void confirmReduce(String productId, int quantity) {
        // 真正扣减:冻结库存-quantity
        Inventory inventory = inventoryRepository.findByProductId(productId);
        inventory.setFrozen(inventory.getFrozen() - quantity);
        inventoryRepository.save(inventory);
        
        log.info("Confirm阶段:库存已扣减,productId={}, quantity={}", productId, quantity);
    }
    
    public void cancelReduce(String productId, int quantity) {
        // 释放冻结:可用库存+quantity,冻结库存-quantity
        Inventory inventory = inventoryRepository.findByProductId(productId);
        inventory.setAvailable(inventory.getAvailable() + quantity);
        inventory.setFrozen(inventory.getFrozen() - quantity);
        inventoryRepository.save(inventory);
        
        log.info("Cancel阶段:库存已释放,productId={}, quantity={}", productId, quantity);
    }
}

// 账户服务
@Service
public class AccountTccService {
    
    @Tcc(confirmMethod = "confirmDeduct", cancelMethod = "cancelDeduct")
    public void tryDeductBalance(String userId, BigDecimal amount) {
        // 冻结金额
        Account account = accountRepository.findByUserId(userId);
        account.setAvailable(account.getAvailable().subtract(amount));
        account.setFrozen(account.getFrozen().add(amount));
        accountRepository.save(account);
        
        log.info("Try阶段:金额已冻结,userId={}, amount={}", userId, amount);
    }
    
    public void confirmDeduct(String userId, BigDecimal amount) {
        // 真正扣款
        Account account = accountRepository.findByUserId(userId);
        account.setFrozen(account.getFrozen().subtract(amount));
        accountRepository.save(account);
        
        log.info("Confirm阶段:金额已扣除,userId={}, amount={}", userId, amount);
    }
    
    public void cancelDeduct(String userId, BigDecimal amount) {
        // 释放冻结
        Account account = accountRepository.findByUserId(userId);
        account.setAvailable(account.getAvailable().add(amount));
        account.setFrozen(account.getFrozen().subtract(amount));
        accountRepository.save(account);
        
        log.info("Cancel阶段:金额已释放,userId={}, amount={}", userId, amount);
    }
}

优点

  • 不依赖数据库层面的事务:每个服务独立管理
  • 性能较好:可以异步化
  • 灵活性高:业务逻辑清晰

缺点

  • 开发成本高:需要实现Try、Confirm、Cancel三个方法
  • 业务侵入性强:需要改造现有业务代码
  • 需要考虑幂等性:Confirm和Cancel可能被重复调用

4️⃣ Saga模式 - 流派 🌊

Saga是目前最流行的分布式事务解决方案之一!把一个大事务拆成多个小事务,每个小事务都有对应的补偿操作。

两种实现方式

📮 1. 事件编排(Choreography)- 去中心化
订单服务 --[OrderCreated事件]--> 库存服务
                                     |
                        [InventoryReduced事件]
                                     ⬇️
                                 账户服务
                                     |
                        [PaymentCompleted事件]
                                     ⬇️
                                   完成!

特点

  • 没有中央协调者
  • 每个服务监听事件,自主决定下一步
  • 就像接力赛跑,每个人跑完自己的棒次
🎯 2. 命令协调(Orchestration)- 中心化
          Saga协调器(总指挥)
               |
    ___________|____________
   |           |            |
订单服务    库存服务     账户服务

特点

  • 有一个中央协调者
  • 协调者负责调用各个服务,管理整个流程
  • 就像乐队指挥,统一协调

🎭 用拍电影理解Saga

正常流程

导演:"Action!"
演员A:演完第一场 ✅
演员B:演完第二场 ✅  
演员C:演完第三场 ✅
导演:"Cut!完美!"

出问题了

导演:"Action!"
演员A:演完第一场 ✅
演员B:演完第二场 ✅
演员C:忘词了!❌
导演:"Cut!重来!"
演员B:重拍第二场(补偿操作)
演员A:重拍第一场(补偿操作)

代码示例(事件编排方式)

// 订单服务
@Service
public class OrderSagaService {
    
    @Autowired
    private EventPublisher eventPublisher;
    
    @Transactional
    public void createOrder(OrderRequest request) {
        try {
            // 1. 创建订单
            Order order = new Order();
            order.setUserId(request.getUserId());
            order.setProductId(request.getProductId());
            order.setQuantity(request.getQuantity());
            order.setAmount(request.getAmount());
            order.setStatus("PENDING");
            orderRepository.save(order);
            
            // 2. 发布订单创建事件
            OrderCreatedEvent event = new OrderCreatedEvent();
            event.setOrderId(order.getId());
            event.setProductId(request.getProductId());
            event.setQuantity(request.getQuantity());
            event.setUserId(request.getUserId());
            event.setAmount(request.getAmount());
            eventPublisher.publish(event);
            
            log.info("订单创建成功,等待后续处理,orderId={}", order.getId());
            
        } catch (Exception e) {
            log.error("订单创建失败", e);
            throw new BusinessException("订单创建失败");
        }
    }
    
    // 监听库存扣减失败事件,进行补偿
    @EventListener
    public void onInventoryReduceFailed(InventoryReduceFailedEvent event) {
        log.warn("库存扣减失败,开始补偿订单,orderId={}", event.getOrderId());
        
        Order order = orderRepository.findById(event.getOrderId());
        order.setStatus("CANCELLED");
        orderRepository.save(order);
        
        log.info("订单补偿完成,orderId={}", event.getOrderId());
    }
}

// 库存服务
@Service
public class InventorySagaService {
    
    @Autowired
    private EventPublisher eventPublisher;
    
    // 监听订单创建事件
    @EventListener
    @Transactional
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            // 1. 扣减库存
            Inventory inventory = inventoryRepository.findByProductId(event.getProductId());
            
            if (inventory.getQuantity() < event.getQuantity()) {
                throw new BusinessException("库存不足");
            }
            
            inventory.setQuantity(inventory.getQuantity() - event.getQuantity());
            inventoryRepository.save(inventory);
            
            // 2. 发布库存扣减成功事件
            InventoryReducedEvent reducedEvent = new InventoryReducedEvent();
            reducedEvent.setOrderId(event.getOrderId());
            reducedEvent.setUserId(event.getUserId());
            reducedEvent.setAmount(event.getAmount());
            eventPublisher.publish(reducedEvent);
            
            log.info("库存扣减成功,orderId={}", event.getOrderId());
            
        } catch (Exception e) {
            log.error("库存扣减失败,orderId={}", event.getOrderId(), e);
            
            // 发布库存扣减失败事件
            InventoryReduceFailedEvent failedEvent = new InventoryReduceFailedEvent();
            failedEvent.setOrderId(event.getOrderId());
            eventPublisher.publish(failedEvent);
        }
    }
    
    // 监听支付失败事件,进行补偿
    @EventListener
    public void onPaymentFailed(PaymentFailedEvent event) {
        log.warn("支付失败,开始补偿库存,orderId={}", event.getOrderId());
        
        // 恢复库存
        OrderCreatedEvent originalEvent = event.getOriginalEvent();
        Inventory inventory = inventoryRepository.findByProductId(originalEvent.getProductId());
        inventory.setQuantity(inventory.getQuantity() + originalEvent.getQuantity());
        inventoryRepository.save(inventory);
        
        log.info("库存补偿完成,orderId={}", event.getOrderId());
    }
}

// 账户服务
@Service
public class AccountSagaService {
    
    @Autowired
    private EventPublisher eventPublisher;
    
    // 监听库存扣减成功事件
    @EventListener
    @Transactional
    public void onInventoryReduced(InventoryReducedEvent event) {
        try {
            // 1. 扣款
            Account account = accountRepository.findByUserId(event.getUserId());
            
            if (account.getBalance().compareTo(event.getAmount()) < 0) {
                throw new BusinessException("余额不足");
            }
            
            account.setBalance(account.getBalance().subtract(event.getAmount()));
            accountRepository.save(account);
            
            // 2. 发布支付成功事件
            PaymentCompletedEvent completedEvent = new PaymentCompletedEvent();
            completedEvent.setOrderId(event.getOrderId());
            eventPublisher.publish(completedEvent);
            
            log.info("支付成功,orderId={}", event.getOrderId());
            
        } catch (Exception e) {
            log.error("支付失败,orderId={}", event.getOrderId(), e);
            
            // 发布支付失败事件
            PaymentFailedEvent failedEvent = new PaymentFailedEvent();
            failedEvent.setOrderId(event.getOrderId());
            failedEvent.setOriginalEvent(event);
            eventPublisher.publish(failedEvent);
        }
    }
}

优点

  • 异步执行:性能好,吞吐量高
  • 松耦合:服务之间通过事件通信
  • 适合长事务:可以跨天的交易也能处理

缺点

  • 最终一致性:不是实时一致
  • 补偿逻辑复杂:要考虑各种异常情况
  • 调试困难:链路长,排查问题麻烦

5️⃣ 本地消息表 - 朴实派 📝

这是一个非常朴实但有效的方案!

原理

订单服务数据库:
+------------+    +----------------+
| 订单表     |    | 本地消息表     |
+------------+    +----------------+
| order_id   |    | msg_id         |
| user_id    |    | topic          |
| amount     |    | content        |
| status     |    | status         |
+------------+    | retry_count    |
                  +----------------+

在同一个事务中:
1. 插入订单记录
2. 插入消息记录

然后:
3. 定时任务扫描消息表
4. 发送消息到MQ
5. 发送成功后,标记消息为已发送

🚚 用快递理解本地消息表

你在网上买了东西:

  1. 下单:商家同时做两件事

    • 记录你的订单(订单表)
    • 记录要给快递公司的通知(消息表)
  2. 发货:商家后台有个小助理

    • 定期检查消息表:"有没有要发的货?"
    • 通知快递公司来取货
    • 快递确认收货后,标记"已发货"
  3. 容错:如果快递没来

    • 小助理会不断重试
    • 直到快递确认收货为止

代码示例

// 订单服务
@Service
public class OrderServiceWithLocalMessage {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private LocalMessageRepository messageRepository;
    
    @Autowired
    private MessageSender messageSender;
    
    @Transactional
    public void createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");
        orderRepository.save(order);
        
        // 2. 在同一个事务中,保存本地消息
        LocalMessage message = new LocalMessage();
        message.setTopic("order-created");
        message.setContent(JSON.toJSONString(order));
        message.setStatus("PENDING");
        message.setRetryCount(0);
        message.setMaxRetry(5);
        messageRepository.save(message);
        
        log.info("订单和消息已保存,orderId={}", order.getId());
    }
    
    // 定时任务:扫描并发送消息
    @Scheduled(fixedDelay = 5000) // 每5秒执行一次
    public void scanAndSendMessage() {
        List<LocalMessage> pendingMessages = messageRepository
            .findByStatusAndRetryCountLessThan("PENDING", 5);
        
        for (LocalMessage message : pendingMessages) {
            try {
                // 发送消息到MQ
                messageSender.send(message.getTopic(), message.getContent());
                
                // 标记为已发送
                message.setStatus("SENT");
                messageRepository.save(message);
                
                log.info("消息发送成功,messageId={}", message.getId());
                
            } catch (Exception e) {
                // 发送失败,增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                
                if (message.getRetryCount() >= message.getMaxRetry()) {
                    message.setStatus("FAILED");
                    log.error("消息发送失败,已达最大重试次数,messageId={}", message.getId());
                }
                
                messageRepository.save(message);
            }
        }
    }
}

// 库存服务:消费消息
@Service
public class InventoryMessageConsumer {
    
    @KafkaListener(topics = "order-created")
    public void onOrderCreated(String message) {
        try {
            Order order = JSON.parseObject(message, Order.class);
            
            // 扣减库存(幂等性处理)
            reduceInventory(order.getProductId(), order.getQuantity());
            
            log.info("库存扣减成功,orderId={}", order.getId());
            
        } catch (Exception e) {
            log.error("库存扣减失败", e);
            // 可以发送补偿消息
        }
    }
    
    private void reduceInventory(String productId, int quantity) {
        // 实现库存扣减逻辑
        // 注意:要保证幂等性!
    }
}

优点

  • 实现简单:不需要额外的框架
  • 可靠性高:消息不会丢失
  • 对现有系统改造小:只需加张表

缺点

  • 耦合度较高:消息表和业务表在一起
  • 需要定时任务:额外的资源消耗
  • 不适合高并发:扫描表有性能瓶颈

6️⃣ 最大努力通知 - 佛系派 🙏

适用于对一致性要求不那么高的场景。

原理

服务A --调用--> 服务B
       |
       |--失败了?没关系!
       |
       |--重试1次
       |--重试2次  
       |--重试3次
       |
       |--还是失败?
       |
       └---> 发送通知,人工处理

🎁 用送礼物理解

你要给朋友送生日礼物:

  1. 第一次送,朋友不在家 ❌
  2. 第二次送,朋友还是不在 ❌
  3. 第三次送,朋友还是不在 ❌
  4. 算了,给朋友打个电话:"礼物放你家门口了,记得拿!"

优点

  • 实现最简单
  • 性能最好

缺点

  • 可靠性最低
  • 可能需要人工介入

🎯 实战篇:订单-库存-账户的血泪史 {#实战篇}

好了,理论讲完了,该来点实战了!让我用一个完整的案例,手把手教你怎么实现!

业务场景

电商系统下单流程: 用户点击"立即购买"→创建订单→扣减库存→扣款→完成

技术选型

经过10年老司机的血泪教训,我推荐:

  • 强一致性场景(如金融交易):选TCC
  • 弱一致性场景(如电商订单):选Saga + 本地消息表
  • 超高并发场景:选最大努力通知

我们这里选择 Saga + 本地消息表 的组合方案!

系统架构图

                     客户端
                       |
                       ⬇️
                   API网关
                       |
         ______________|______________
        |              |              |
        ⬇️              ⬇️              ⬇️
   订单服务        库存服务        账户服务
        |              |              |
        ⬇️              ⬇️              ⬇️
   订单DB          库存DB          账户DB
        |              |              |
        |__________ MQ _____________|
             (Kafka/RocketMQ)

数据库设计

订单服务

-- 订单表
CREATE TABLE `t_order` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
  `user_id` BIGINT COMMENT '用户ID',
  `product_id` BIGINT COMMENT '商品ID',
  `quantity` INT COMMENT '数量',
  `amount` DECIMAL(10,2) COMMENT '金额',
  `status` VARCHAR(20) COMMENT '状态:PENDING/SUCCESS/FAILED/CANCELLED',
  `create_time` DATETIME,
  `update_time` DATETIME,
  INDEX idx_user_id (`user_id`),
  INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='订单表';

-- 本地消息表
CREATE TABLE `t_local_message` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `message_id` VARCHAR(64) UNIQUE COMMENT '消息ID',
  `topic` VARCHAR(100) COMMENT '主题',
  `content` TEXT COMMENT '消息内容',
  `status` VARCHAR(20) COMMENT '状态:PENDING/SENT/FAILED',
  `retry_count` INT DEFAULT 0 COMMENT '重试次数',
  `max_retry` INT DEFAULT 5 COMMENT '最大重试次数',
  `create_time` DATETIME,
  `update_time` DATETIME,
  INDEX idx_status (`status`)
) ENGINE=InnoDB COMMENT='本地消息表';

库存服务

-- 库存表
CREATE TABLE `t_inventory` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `product_id` BIGINT UNIQUE COMMENT '商品ID',
  `quantity` INT COMMENT '库存数量',
  `version` INT COMMENT '版本号(乐观锁)',
  `create_time` DATETIME,
  `update_time` DATETIME,
  INDEX idx_product_id (`product_id`)
) ENGINE=InnoDB COMMENT='库存表';

-- 库存操作日志表(防止重复扣减)
CREATE TABLE `t_inventory_log` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
  `product_id` BIGINT COMMENT '商品ID',
  `quantity` INT COMMENT '扣减数量',
  `type` VARCHAR(20) COMMENT '类型:REDUCE/RESTORE',
  `create_time` DATETIME,
  INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='库存操作日志';

账户服务

-- 账户表
CREATE TABLE `t_account` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `user_id` BIGINT UNIQUE COMMENT '用户ID',
  `balance` DECIMAL(10,2) COMMENT '余额',
  `version` INT COMMENT '版本号(乐观锁)',
  `create_time` DATETIME,
  `update_time` DATETIME,
  INDEX idx_user_id (`user_id`)
) ENGINE=InnoDB COMMENT='账户表';

-- 账户操作日志表(防止重复扣款)
CREATE TABLE `t_account_log` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `order_no` VARCHAR(64) UNIQUE COMMENT '订单号',
  `user_id` BIGINT COMMENT '用户ID',
  `amount` DECIMAL(10,2) COMMENT '金额',
  `type` VARCHAR(20) COMMENT '类型:DEDUCT/REFUND',
  `create_time` DATETIME,
  INDEX idx_order_no (`order_no`)
) ENGINE=InnoDB COMMENT='账户操作日志';

完整代码实现

1. 订单服务(核心)

@Service
@Slf4j
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    /**
     * 创建订单(Saga的第一步)
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(CreateOrderRequest request) {
        // 1. 生成订单号
        String orderNo = generateOrderNo();
        
        // 2. 创建订单
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");
        orderMapper.insert(order);
        
        // 3. 在同一事务中保存本地消息(关键!)
        LocalMessage message = new LocalMessage();
        message.setMessageId(UUID.randomUUID().toString());
        message.setTopic("order-created");
        
        // 消息内容
        OrderCreatedEvent event = new OrderCreatedEvent();
        event.setOrderNo(orderNo);
        event.setUserId(request.getUserId());
        event.setProductId(request.getProductId());
        event.setQuantity(request.getQuantity());
        event.setAmount(request.getAmount());
        message.setContent(JSON.toJSONString(event));
        
        message.setStatus("PENDING");
        message.setRetryCount(0);
        message.setMaxRetry(5);
        messageMapper.insert(message);
        
        log.info("订单创建成功,orderNo={}", orderNo);
        
        return orderNo;
    }
    
    /**
     * 监听库存扣减失败事件,进行补偿
     */
    @KafkaListener(topics = "inventory-reduce-failed")
    public void onInventoryReduceFailed(String message) {
        InventoryReduceFailedEvent event = JSON.parseObject(message, InventoryReduceFailedEvent.class);
        
        log.warn("库存扣减失败,开始补偿订单,orderNo={}", event.getOrderNo());
        
        // 更新订单状态为FAILED
        Order order = orderMapper.selectByOrderNo(event.getOrderNo());
        if (order != null) {
            order.setStatus("FAILED");
            orderMapper.updateById(order);
            log.info("订单补偿完成,orderNo={}", event.getOrderNo());
        }
    }
    
    /**
     * 监听支付失败事件,进行补偿
     */
    @KafkaListener(topics = "payment-failed")
    public void onPaymentFailed(String message) {
        PaymentFailedEvent event = JSON.parseObject(message, PaymentFailedEvent.class);
        
        log.warn("支付失败,开始补偿订单,orderNo={}", event.getOrderNo());
        
        // 更新订单状态为FAILED
        Order order = orderMapper.selectByOrderNo(event.getOrderNo());
        if (order != null) {
            order.setStatus("FAILED");
            orderMapper.updateById(order);
            log.info("订单补偿完成,orderNo={}", event.getOrderNo());
        }
    }
    
    /**
     * 监听支付成功事件
     */
    @KafkaListener(topics = "payment-completed")
    public void onPaymentCompleted(String message) {
        PaymentCompletedEvent event = JSON.parseObject(message, PaymentCompletedEvent.class);
        
        log.info("支付成功,更新订单状态,orderNo={}", event.getOrderNo());
        
        // 更新订单状态为SUCCESS
        Order order = orderMapper.selectByOrderNo(event.getOrderNo());
        if (order != null) {
            order.setStatus("SUCCESS");
            orderMapper.updateById(order);
            log.info("订单完成,orderNo={}", event.getOrderNo());
        }
    }
    
    private String generateOrderNo() {
        // 生成订单号:时间戳 + 随机数
        return "ORD" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
    }
}

2. 消息发送定时任务

@Component
@Slf4j
public class MessageSendScheduler {
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 定时扫描并发送消息
     */
    @Scheduled(fixedDelay = 5000) // 每5秒执行一次
    public void scanAndSendMessage() {
        // 查询待发送的消息
        List<LocalMessage> messages = messageMapper.selectPendingMessages();
        
        if (messages.isEmpty()) {
            return;
        }
        
        log.info("开始发送消息,数量={}", messages.size());
        
        for (LocalMessage message : messages) {
            try {
                // 发送消息到Kafka
                kafkaTemplate.send(message.getTopic(), message.getContent()).get();
                
                // 标记为已发送
                message.setStatus("SENT");
                messageMapper.updateById(message);
                
                log.info("消息发送成功,messageId={}, topic={}", 
                    message.getMessageId(), message.getTopic());
                
            } catch (Exception e) {
                log.error("消息发送失败,messageId={}", message.getMessageId(), e);
                
                // 增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                
                // 超过最大重试次数,标记为失败
                if (message.getRetryCount() >= message.getMaxRetry()) {
                    message.setStatus("FAILED");
                    log.error("消息发送失败,已达最大重试次数,messageId={}", message.getMessageId());
                    
                    // TODO: 发送告警,人工介入
                }
                
                messageMapper.updateById(message);
            }
        }
    }
}

3. 库存服务

@Service
@Slf4j
public class InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    @Autowired
    private InventoryLogMapper inventoryLogMapper;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 监听订单创建事件,扣减库存
     */
    @KafkaListener(topics = "order-created")
    @Transactional(rollbackFor = Exception.class)
    public void onOrderCreated(String message) {
        OrderCreatedEvent event = JSON.parseObject(message, OrderCreatedEvent.class);
        
        log.info("收到订单创建事件,开始扣减库存,orderNo={}", event.getOrderNo());
        
        try {
            // 幂等性检查:判断是否已经处理过
            InventoryLog existLog = inventoryLogMapper.selectByOrderNo(event.getOrderNo());
            if (existLog != null) {
                log.warn("订单已处理过,跳过,orderNo={}", event.getOrderNo());
                return;
            }
            
            // 扣减库存(乐观锁)
            Inventory inventory = inventoryMapper.selectByProductId(event.getProductId());
            
            if (inventory == null) {
                throw new BusinessException("商品不存在");
            }
            
            if (inventory.getQuantity() < event.getQuantity()) {
                throw new BusinessException("库存不足");
            }
            
            int rows = inventoryMapper.reduceInventory(
                event.getProductId(), 
                event.getQuantity(), 
                inventory.getVersion()
            );
            
            if (rows == 0) {
                // 乐观锁失败,重试
                throw new BusinessException("库存扣减失败,请重试");
            }
            
            // 记录日志
            InventoryLog log = new InventoryLog();
            log.setOrderNo(event.getOrderNo());
            log.setProductId(event.getProductId());
            log.setQuantity(event.getQuantity());
            log.setType("REDUCE");
            inventoryLogMapper.insert(log);
            
            // 发送库存扣减成功事件
            InventoryReducedEvent reducedEvent = new InventoryReducedEvent();
            reducedEvent.setOrderNo(event.getOrderNo());
            reducedEvent.setUserId(event.getUserId());
            reducedEvent.setAmount(event.getAmount());
            kafkaTemplate.send("inventory-reduced", JSON.toJSONString(reducedEvent));
            
            this.log.info("库存扣减成功,orderNo={}, productId={}, quantity={}", 
                event.getOrderNo(), event.getProductId(), event.getQuantity());
            
        } catch (Exception e) {
            log.error("库存扣减失败,orderNo={}", event.getOrderNo(), e);
            
            // 发送库存扣减失败事件
            InventoryReduceFailedEvent failedEvent = new InventoryReduceFailedEvent();
            failedEvent.setOrderNo(event.getOrderNo());
            failedEvent.setReason(e.getMessage());
            kafkaTemplate.send("inventory-reduce-failed", JSON.toJSONString(failedEvent));
        }
    }
    
    /**
     * 监听支付失败事件,恢复库存
     */
    @KafkaListener(topics = "payment-failed")
    @Transactional(rollbackFor = Exception.class)
    public void onPaymentFailed(String message) {
        PaymentFailedEvent event = JSON.parseObject(message, PaymentFailedEvent.class);
        
        log.warn("支付失败,开始恢复库存,orderNo={}", event.getOrderNo());
        
        try {
            // 查询原始扣减记录
            InventoryLog reduceLog = inventoryLogMapper.selectByOrderNoAndType(
                event.getOrderNo(), "REDUCE");
            
            if (reduceLog == null) {
                log.warn("未找到扣减记录,orderNo={}", event.getOrderNo());
                return;
            }
            
            // 幂等性检查:判断是否已经恢复过
            InventoryLog restoreLog = inventoryLogMapper.selectByOrderNoAndType(
                event.getOrderNo(), "RESTORE");
            
            if (restoreLog != null) {
                log.warn("库存已恢复过,跳过,orderNo={}", event.getOrderNo());
                return;
            }
            
            // 恢复库存
            inventoryMapper.addInventory(
                reduceLog.getProductId(), 
                reduceLog.getQuantity()
            );
            
            // 记录恢复日志
            InventoryLog log = new InventoryLog();
            log.setOrderNo(event.getOrderNo());
            log.setProductId(reduceLog.getProductId());
            log.setQuantity(reduceLog.getQuantity());
            log.setType("RESTORE");
            inventoryLogMapper.insert(log);
            
            this.log.info("库存恢复成功,orderNo={}, productId={}, quantity={}", 
                event.getOrderNo(), reduceLog.getProductId(), reduceLog.getQuantity());
            
        } catch (Exception e) {
            log.error("库存恢复失败,orderNo={}", event.getOrderNo(), e);
        }
    }
}

4. 账户服务

@Service
@Slf4j
public class AccountService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Autowired
    private AccountLogMapper accountLogMapper;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    /**
     * 监听库存扣减成功事件,进行扣款
     */
    @KafkaListener(topics = "inventory-reduced")
    @Transactional(rollbackFor = Exception.class)
    public void onInventoryReduced(String message) {
        InventoryReducedEvent event = JSON.parseObject(message, InventoryReducedEvent.class);
        
        log.info("收到库存扣减成功事件,开始扣款,orderNo={}", event.getOrderNo());
        
        try {
            // 幂等性检查
            AccountLog existLog = accountLogMapper.selectByOrderNo(event.getOrderNo());
            if (existLog != null) {
                log.warn("订单已处理过,跳过,orderNo={}", event.getOrderNo());
                return;
            }
            
            // 扣款(乐观锁)
            Account account = accountMapper.selectByUserId(event.getUserId());
            
            if (account == null) {
                throw new BusinessException("账户不存在");
            }
            
            if (account.getBalance().compareTo(event.getAmount()) < 0) {
                throw new BusinessException("余额不足");
            }
            
            int rows = accountMapper.deductBalance(
                event.getUserId(), 
                event.getAmount(), 
                account.getVersion()
            );
            
            if (rows == 0) {
                throw new BusinessException("扣款失败,请重试");
            }
            
            // 记录日志
            AccountLog log = new AccountLog();
            log.setOrderNo(event.getOrderNo());
            log.setUserId(event.getUserId());
            log.setAmount(event.getAmount());
            log.setType("DEDUCT");
            accountLogMapper.insert(log);
            
            // 发送支付成功事件
            PaymentCompletedEvent completedEvent = new PaymentCompletedEvent();
            completedEvent.setOrderNo(event.getOrderNo());
            kafkaTemplate.send("payment-completed", JSON.toJSONString(completedEvent));
            
            this.log.info("扣款成功,orderNo={}, userId={}, amount={}", 
                event.getOrderNo(), event.getUserId(), event.getAmount());
            
        } catch (Exception e) {
            log.error("扣款失败,orderNo={}", event.getOrderNo(), e);
            
            // 发送支付失败事件
            PaymentFailedEvent failedEvent = new PaymentFailedEvent();
            failedEvent.setOrderNo(event.getOrderNo());
            failedEvent.setReason(e.getMessage());
            kafkaTemplate.send("payment-failed", JSON.toJSONString(failedEvent));
        }
    }
}

时序图

用户      订单服务      MQ      库存服务      账户服务
 |           |          |          |            |
 |--下单---->|          |          |            |
 |           |          |          |            |
 |           |--保存订单+消息      |            |
 |           |(同一事务)  |         |            |
 |           |          |          |            |
 |<--成功----|          |          |            |
 |           |          |          |            |
 |          定时任务     |          |            |
 |           |--发送消息->|         |            |
 |           |          |          |            |
 |           |          |--订单创建事件-->       |
 |           |          |          |            |
 |           |          |      扣减库存         |
 |           |          |          |            |
 |           |          |<-库存扣减成功事件--    |
 |           |          |          |            |
 |           |          |--库存扣减成功事件----->|
 |           |          |          |         扣款
 |           |          |          |            |
 |           |          |<------支付成功事件-----|
 |           |          |          |            |
 |           |<-支付成功事件        |            |
 |           |          |          |            |
 |       更新订单状态   |          |            |
 |           |          |          |            |

🚨 避坑指南:我踩过的那些坑 {#避坑指南}

坑1:忘记幂等性处理 😭

场景:消息可能会重复消费,导致重复扣库存、重复扣款!

解决方案

// 方案1:唯一键约束
CREATE UNIQUE INDEX uk_order_no ON t_inventory_log(order_no);

// 方案2:先查询再插入
InventoryLog existLog = inventoryLogMapper.selectByOrderNo(orderNo);
if (existLog != null) {
    log.warn("Already processed, skip");
    return;
}

坑2:忘记处理消息发送失败 😱

场景:订单保存成功了,但消息没发出去,后续流程卡住了!

解决方案:本地消息表 + 定时任务扫描重试

坑3:补偿操作没做好 🤦

场景:支付失败了,但库存没恢复!

解决方案

// 每个正向操作都要有对应的补偿操作
正向:reduceInventory() 
补偿:restoreInventory()

正向:deductBalance()
补偿:refundBalance()

坑4:死锁问题 💀

场景:高并发下,乐观锁失败率太高,导致大量重试!

解决方案

// 方案1:限流
@RateLimiter(value = 100, timeout = 1000)

// 方案2:分段锁(把库存分成多份)
// 原来:商品A总库存100
// 现在:商品A库存分成10份,每份10个

坑5:消息堆积 📮

场景:下游服务挂了,消息堆积几百万条!

解决方案

// 1. 设置消息过期时间
message.setExpireTime(30 * 60 * 1000); // 30分钟

// 2. 死信队列
@KafkaListener(topics = "order-created-dlq")
public void handleDeadLetter(String message) {
    // 人工处理
}

// 3. 限流
@RateLimiter

坑6:分布式锁的滥用 🔒

错误示例

// 千万别这样!
String lockKey = "inventory:" + productId;
if (redisLock.tryLock(lockKey)) {
    try {
        reduceInventory();
    } finally {
        redisLock.unlock(lockKey);
    }
}

问题

  • 锁的粒度太粗,性能差
  • 锁超时了咋办?
  • 锁被别人释放了咋办?

正确做法:用乐观锁(version字段)

UPDATE t_inventory 
SET quantity = quantity - #{quantity},
    version = version + 1
WHERE product_id = #{productId}
  AND version = #{version}
  AND quantity >= #{quantity}

🚀 性能优化:从龟速到飞起 {#性能优化}

优化1:异步化 ⚡

Before

// 同步调用,慢死了
createOrder();
reduceInventory(); // 等待
deductBalance();   // 等待

After

// 异步调用,飞快
createOrder();
发送消息("order-created"); // 不等待,直接返回

效果:RT从3000ms降到100ms!

优化2:批量处理 📦

Before

for (Message msg : messages) {
    kafkaTemplate.send(topic, msg); // 一条一条发
}

After

List<Message> batch = new ArrayList<>();
for (Message msg : messages) {
    batch.add(msg);
    if (batch.size() >= 100) {
        kafkaTemplate.send(topic, batch); // 批量发送
        batch.clear();
    }
}

效果:TPS从1000提升到10000!

优化3:缓存预热 🔥

// 把热点商品的库存放到Redis
String cacheKey = "inventory:" + productId;
Integer stock = redisTemplate.get(cacheKey);

if (stock == null) {
    // 缓存未命中,查数据库
    stock = inventoryMapper.selectByProductId(productId).getQuantity();
    redisTemplate.set(cacheKey, stock, 60, TimeUnit.SECONDS);
}

效果:数据库压力降低90%!

优化4:消息压缩 📉

// 消息太大,网络传输慢
String message = JSON.toJSONString(event);

// 压缩
byte[] compressed = gzip(message.getBytes());

// 发送压缩后的消息
kafkaTemplate.send(topic, compressed);

效果:网络带宽占用降低70%!

优化5:数据库优化 💾

-- 添加索引
CREATE INDEX idx_status ON t_local_message(status);
CREATE INDEX idx_order_no ON t_order(order_no);
CREATE INDEX idx_product_id ON t_inventory(product_id);

-- 分区表(按时间分区)
CREATE TABLE t_order (
  ...
) PARTITION BY RANGE (YEAR(create_time)) (
  PARTITION p2023 VALUES LESS THAN (2024),
  PARTITION p2024 VALUES LESS THAN (2025),
  PARTITION p2025 VALUES LESS THAN (2026)
);

-- 定期清理历史数据
DELETE FROM t_local_message 
WHERE status = 'SENT' 
  AND create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);

🎓 企业级框架推荐

Seata - 阿里出品 🌟

官网seata.io/

支持的模式

  • AT模式(自动)
  • TCC模式(手动)
  • Saga模式(长事务)
  • XA模式(强一致)

快速开始

<!-- 引入依赖 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>
// 只需加一个注解!
@GlobalTransactional
public void placeOrder() {
    orderService.createOrder();
    inventoryService.reduceInventory();
    accountService.deductBalance();
}

优点

  • ✅ 阿里生产环境验证
  • ✅ 支持多种模式
  • ✅ 对业务侵入小

缺点

  • ❌ 需要部署TC(事务协调器)
  • ❌ 学习成本较高

Apache ServiceComb Pack

官网servicecomb.apache.org/

特点

  • 支持Saga模式
  • Apache顶级项目
  • 文档完善

自研方案

如果你的业务比较特殊,也可以自研!

核心组件

  1. 本地消息表
  2. 定时任务
  3. 消息队列
  4. 补偿逻辑

🎯 选型决策树

开始
 |
 ⬇️
是否需要强一致性?
 |        \
         
 |          \
 ⬇️          ⬇️
TCC       是否能接受最终一致?
 |          |        \
 |                  
 |          |          \
 |          ⬇️          ⬇️
 |       Saga      重新评估需求
 |       +本地消息表
 |          |
 |          ⬇️
 |      是否高并发?
 |       |        \
 |      是         否
 |       |          \
 |       ⬇️          ⬇️
 |    + Redis    基础方案
 |    + 分段锁
 |       |
 └-------┴--------⬇️
          生产可用!

📚 总结

核心要点

  1. 理论基础

    • CAP定理:不可能三角
    • BASE理论:最终一致性
    • ACID vs BASE
  2. 方案选择

    • 强一致性 → TCC
    • 弱一致性 → Saga
    • 超高并发 → 异步化
  3. 关键技术

    • 本地消息表
    • 幂等性处理
    • 补偿机制
    • 乐观锁
  4. 避坑指南

    • 幂等性!幂等性!幂等性!
    • 补偿操作要完善
    • 消息堆积要处理
    • 性能优化要到位

最佳实践

/**
 * 分布式事务最佳实践清单
 */1. 每个操作都要有幂等性
✅ 2. 每个正向操作都要有补偿操作
✅ 3. 消息发送要可靠(本地消息表)
✅ 4. 要有重试机制(指数退避)
✅ 5. 要有超时控制
✅ 6. 要有监控告警
✅ 7. 要有降级方案
✅ 8. 要有人工介入机制
✅ 9. 数据要有日志记录
✅ 10. 要有压测验证

🎤 老司机的肺腑之言

作为一个被分布式事务折磨过无数次的10年老Java程序员,我想说:

  1. 不要过度设计
    能用单体就用单体,别一上来就微服务!

  2. 选择合适的方案
    没有银弹,只有最合适的!

  3. 一定要做好测试
    正常流程测、异常流程测、并发测、压力测!

  4. 监控告警很重要
    出问题能第一时间发现,别等用户投诉!

  5. 文档要写好
    半年后你自己都看不懂,别说新人了!

  6. 持续优化
    上线不是终点,是起点!


🔗 参考资料

  1. 《分布式系统原理与范型》- Andrew S. Tanenbaum
  2. 《数据密集型应用系统设计》- Martin Kleppmann
  3. Seata官方文档:seata.io/
  4. 《微服务设计》- Sam Newman
  5. 阿里技术公众号系列文章

📮 最后

如果这篇文档对你有帮助,请点个赞👍!

如果你有问题,欢迎评论区交流!

如果你也踩过类似的坑,欢迎分享你的经验!

记住:分布式事务不可怕,可怕的是不了解它就乱用!😄


后记:写这篇文档的时候,我又想起了那个被分布式事务支配的恐惧...
不过还好,现在的我已经是个老司机了!💪
希望这篇文档能帮你少走弯路,少踩坑!

Keep Coding, Keep Learning! 🚀

—— 一个秃了但变强了的Java程序员


全文完