每日Java面试场景题知识点之-分布式事务

每日Java面试场景题知识点之-分布式事务

一、分布式事务问题由来

在单体应用时代,我们可以依赖数据库本地事务(ACID)来保证数据的一致性。一个业务操作涉及的多个数据库更新操作可以放在同一个事务中,要么全部成功,要么全部回滚。

然而随着业务发展,系统拆分为多个微服务后,情况变得复杂起来。一个业务操作可能需要调用多个微服务,每个微服务操作自己的数据库。此时,本地事务已经无法跨服务保证数据一致性,这就是分布式事务问题的由来。

典型场景举例

电商下单场景:用户下单后需要同时完成以下操作

  1. 在订单服务创建订单记录
  2. 调用库存服务扣减库存
  3. 调用账户服务扣减余额
  4. 调用积分服务增加用户积分

这些操作涉及多个独立的数据源,任何一个环节失败都会导致数据不一致。如果订单创建成功但库存扣减失败,就会出现超卖问题;如果库存扣减成功但余额扣减失败,就会出现用户未付款但库存已扣减的问题。

二、分布式事务的核心挑战

2.1 CAP理论

CAP理论指出,一个分布式系统最多只能同时满足以下三个特性中的两个:

  1. 一致性(Consistency):所有节点同时看到相同的数据
  2. 可用性(Availability):每次请求都能得到响应(成功或失败)
  3. 分区容错性(Partition Tolerance):系统在网络分区的情况下仍能继续运行

在分布式系统中,网络分区是不可避免的(P),因此我们只能在CP(一致性和分区容错)和AP(可用性和分区容错)之间做权衡。

2.2 BASE理论

BASE理论是对CAP理论的补充,提出基本可用、软状态和最终一致性的概念,适合互联网场景下的分布式事务处理。

  1. 基本可用(Basically Available):系统出现故障时,允许损失部分可用性
  2. 软状态(Soft State):允许数据存在中间状态,不影响系统可用性
  3. 最终一致性(Eventually Consistent):经过一段时间后,所有节点的数据最终达到一致

三、分布式事务解决方案详解

3.1 两阶段提交(2PC)

两阶段提交是最经典的强一致性分布式事务协议,由协调者和参与者组成。

第一阶段:准备阶段
  1. 协调者向所有参与者发送准备请求
  2. 参与者执行事务操作但不提交
  3. 参与者向协调者反馈是否可以提交
第二阶段:提交阶段
  1. 如果所有参与者都同意提交,协调者发送提交指令
  2. 如果有任一参与者拒绝,协调者发送回滚指令
  3. 参与者根据指令执行提交或回滚
2PC的缺点
  1. 同步阻塞:参与者在等待协调者指令期间处于阻塞状态
  2. 单点故障:协调者故障会导致所有参与者阻塞
  3. 数据不一致:协调者发送提交指令后部分参与者宕机

3.2 三阶段提交(3PC)

3PC在2PC的基础上增加了预准备阶段,降低了阻塞时间。

阶段划分
  1. CanCommit:协调者询问参与者是否可以执行事务
  2. PreCommit:参与者预执行事务,锁定资源
  3. DoCommit:协调者根据参与者反馈决定提交或回滚
3PC的改进与局限

改进点:引入超时机制,参与者可以自主决策 局限性:仍然存在单点故障风险,且增加了网络通信开销

3.3 TCC(Try-Confirm-Cancel)

TCC是应用层的分布式事务解决方案,每个业务操作需要实现三个方法。

TCC工作流程
  1. Try阶段:预留资源,检查业务条件
  2. Confirm阶段:确认执行,使用Try阶段预留的资源
  3. Cancel阶段:取消操作,释放Try阶段预留的资源
TCC示例(订单支付场景)
java 复制代码
public interface OrderService {
    // Try阶段:创建预扣款订单
    @Compensable
    boolean prepareOrder(Order order);
    
    // Confirm阶段:确认订单
    boolean confirmOrder(Order order);
    
    // Cancel阶段:取消订单
    boolean cancelOrder(Order order);
}

public interface AccountService {
    // Try阶段:预扣余额
    @Compensable
    boolean prepareDeduct(String userId, BigDecimal amount);
    
    // Confirm阶段:确认扣款
    boolean confirmDeduct(String userId, BigDecimal amount);
    
    // Cancel阶段:取消扣款
    boolean cancelDeduct(String userId, BigDecimal amount);
}
TCC优缺点

优点:性能较好,不依赖数据库本地事务 缺点:业务代码侵入性强,需要实现三个方法,开发成本高

3.4 Saga模式

Saga将长事务拆分为多个本地短事务,每个短事务都有对应的补偿事务。

Saga执行方式
  1. 正向执行:依次执行各个本地事务
  2. 补偿执行:如果某一步失败,按相反顺序执行补偿事务
Saga实现示例
java 复制代码
public class OrderSaga {
    
    public void createOrder(Order order) {
        try {
            // 步骤1:创建订单
            orderService.create(order);
            
            // 步骤2:扣减库存
            inventoryService.deduct(order.getProductId(), order.getQuantity());
            
            // 步骤3:扣减余额
            accountService.deduct(order.getUserId(), order.getAmount());
            
        } catch (Exception e) {
            // 补偿操作
            accountService.compensateDeduct(order.getUserId(), order.getAmount());
            inventoryService.compensateDeduct(order.getProductId(), order.getQuantity());
            orderService.compensateCreate(order.getId());
            throw e;
        }
    }
}
Saga优缺点

优点:适合长业务流程,可以跨多个服务 缺点:无法保证隔离性,存在脏读问题

3.5 本地消息表

本地消息表方案利用本地事务保证业务操作和消息发送的原子性。

实现原理
  1. 业务操作和消息记录在同一个本地事务中写入
  2. 定时任务扫描消息表,发送未确认的消息
  3. 消费者消费成功后更新消息状态
本地消息表示例
java 复制代码
@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private MessageMapper messageMapper;
    
    @Autowired
    private MessageProducer messageProducer;
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);
        
        // 2. 创建消息记录(与订单在同一个事务中)
        Message message = new Message();
        message.setTopic("order.created");
        message.setContent(JSON.toJSONString(order));
        message.setStatus(MessageStatus.SENDING);
        messageMapper.insert(message);
    }
}

@Component
public class MessageTask {
    
    @Scheduled(fixedDelay = 5000)
    public void sendPendingMessages() {
        List<Message> messages = messageMapper.selectByStatus(MessageStatus.SENDING);
        
        for (Message message : messages) {
            try {
                messageProducer.send(message.getTopic(), message.getContent());
                messageMapper.updateStatus(message.getId(), MessageStatus.SENT);
            } catch (Exception e) {
                log.error("发送消息失败", e);
            }
        }
    }
}

3.6 事务消息

事务消息利用消息中间件(如RocketMQ)的事务消息机制保证最终一致性。

事务消息流程
  1. 发送半消息:消息不可见,不参与消费
  2. 执行本地事务
  3. 提交或回滚消息
  4. 消息中间件回查事务状态
RocketMQ事务消息示例
java 复制代码
@Service
public class OrderTransactionListener implements TransactionListener {
    
    @Autowired
    private OrderService orderService;
    
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            Order order = JSON.parseObject(new String(msg.getBody()), Order.class);
            orderService.createOrder(order);
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
    
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Order order = orderService.queryByTxId(msg.getTransactionId());
        if (order != null) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return LocalTransactionState.UNKNOW;
    }
}

@Component
public class OrderMessageProducer {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    public void sendOrderMessage(Order order) {
        Message<Order> message = MessageBuilder.withPayload(order).build();
        rocketMQTemplate.sendMessageInTransaction(
            "order-group", 
            "order-topic", 
            message, 
            null
        );
    }
}

3.7 Seata分布式事务框架

Seata是阿里巴巴开源的分布式事务解决方案,支持多种事务模式。

Seata架构
  1. TC(Transaction Coordinator):事务协调器
  2. TM(Transaction Manager):事务管理器
  3. RM(Resource Manager):资源管理器
Seata AT模式示例
java 复制代码
@Service
public class OrderServiceImpl {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private AccountService accountService;
    
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 创建订单
        orderMapper.insert(order);
        
        // 调用库存服务(远程调用,自动纳入全局事务)
        inventoryService.deduct(order.getProductId(), order.getQuantity());
        
        // 调用账户服务(远程调用,自动纳入全局事务)
        accountService.deduct(order.getUserId(), order.getAmount());
    }
}
Seata TCC模式示例
java 复制代码
@LocalTCC
public interface InventoryService {
    
    @TwoPhaseBusinessAction(name = "prepareDeduct", commitMethod = "confirmDeduct", rollbackMethod = "cancelDeduct")
    boolean prepareDeduct(@BusinessActionContextParameter(paramName = "productId") String productId, 
                          @BusinessActionContextParameter(paramName = "quantity") int quantity);
    
    boolean confirmDeduct(BusinessActionContext context);
    
    boolean cancelDeduct(BusinessActionContext context);
}

@Service
public class InventoryServiceImpl implements InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    @Autowired
    private InventoryReservedMapper reservedMapper;
    
    @Override
    public boolean prepareDeduct(String productId, int quantity) {
        // 检查库存
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }
        
        // 预留库存
        InventoryReserved reserved = new InventoryReserved();
        reserved.setProductId(productId);
        reserved.setQuantity(quantity);
        reservedMapper.insert(reserved);
        
        return true;
    }
    
    @Override
    public boolean confirmDeduct(BusinessActionContext context) {
        String productId = context.getActionContext("productId").toString();
        int quantity = Integer.parseInt(context.getActionContext("quantity").toString());
        
        // 实际扣减库存
        inventoryMapper.deductStock(productId, quantity);
        
        // 删除预留记录
        reservedMapper.deleteByBizKey(context.getActionContext("bizKey").toString());
        
        return true;
    }
    
    @Override
    public boolean cancelDeduct(BusinessActionContext context) {
        // 删除预留记录
        reservedMapper.deleteByBizKey(context.getActionContext("bizKey").toString());
        return true;
    }
}

四、分布式事务方案对比

4.1 强一致性方案

2PC、3PC、XA:保证强一致性,但性能较差,阻塞严重,适合对一致性要求极高且并发量不大的场景。

4.2 最终一致性方案

TCC、Saga、本地消息表、事务消息:保证最终一致性,性能较好,适合互联网高并发场景。

4.3 方案选择建议

  1. 高并发、允许最终不一致:TCC、Saga、事务消息
  2. 一致性要求高、并发量不大:2PC、XA、Seata AT
  3. 跨服务长流程:Saga模式
  4. 简单场景:本地消息表

五、分布式事务最佳实践

5.1 设计原则

  1. 尽量避免分布式事务:通过业务设计减少跨服务事务
  2. 选择合适的一致性级别:根据业务需求选择强一致或最终一致
  3. 幂等性设计:所有操作都要支持幂等
  4. 补偿机制:设计合理的补偿操作

5.2 注意事项

  1. 超时设置:合理设置超时时间,避免长时间阻塞
  2. 重试机制:设计合理的重试策略
  3. 监控告警:对分布式事务执行情况进行监控
  4. 日志记录:完整记录事务执行过程,便于排查问题

六、面试高频问题

  1. 什么是分布式事务?为什么需要分布式事务?
  2. CAP理论和BASE理论分别是什么?
  3. 2PC和3PC的区别是什么?
  4. TCC的实现原理是什么?有哪些优缺点?
  5. Saga模式如何保证最终一致性?
  6. 本地消息表和事务消息有什么区别?
  7. Seata支持哪些事务模式?AT模式和TCC模式的区别是什么?
  8. 如何选择合适的分布式事务方案?

感谢读者观看

相关推荐
kvo7f2JTy2 小时前
JAVA 设计模式
java·开发语言·设计模式
仍然.2 小时前
多线程---阻塞队列收尾和线程池
java·开发语言·算法
鱼鳞_3 小时前
Java学习笔记_Day22
java·笔记·学习
__土块__3 小时前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统
她说..3 小时前
Java 注解核心面试题
java·spring boot·spring·spring cloud·自定义注解
用户8307196840823 小时前
Spring Boot @Qualifier深度解密:从“按名查找”到“分组批量注入”,一文掌握它的全部“隐藏技能”。
java·spring boot
亦暖筑序3 小时前
Message 四分天下:Spring AI 如何统一消息格式
java·人工智能
镜花水月linyi3 小时前
JDK 8 → 17 → 21 → 25:一次性讲清四代版本的关键跃迁
java·后端
0xDevNull4 小时前
JDK 25 新特性概览与实战教程
java·开发语言·后端