分布式面试题库

文章目录

    • [1. CAP 定理和 BASE 理论是什么?](#1. CAP 定理和 BASE 理论是什么?)
    • [2. 分布式事务有哪些实现方案?](#2. 分布式事务有哪些实现方案?)
      • [1. 2PC(两阶段提交)](#1. 2PC(两阶段提交))
      • [2. 3PC(三阶段提交)](#2. 3PC(三阶段提交))
      • [3. TCC 模式(Try-Confirm-Cancel)](#3. TCC 模式(Try-Confirm-Cancel))
      • [4. Saga 模式](#4. Saga 模式)
      • [5. 本地消息表](#5. 本地消息表)
        • [1. 本地事务执行(原子性保证)](#1. 本地事务执行(原子性保证))
        • [2. 消息发送(定时任务)](#2. 消息发送(定时任务))
        • [3. 消费者处理(最终一致性)](#3. 消费者处理(最终一致性))
        • [4. 补偿机制(处理失败场景)](#4. 补偿机制(处理失败场景))
      • [6. Seata 框架](#6. Seata 框架)
    • [3. Seata 的 AT 模式原理是什么?](#3. Seata 的 AT 模式原理是什么?)
    • [4. TCC 模式的原理和适用场景?](#4. TCC 模式的原理和适用场景?)
    • [4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?](#4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?)
      • [1. 幂等性实现方式](#1. 幂等性实现方式)
      • [2. 空回滚实现方式](#2. 空回滚实现方式)
      • [3. 防悬挂实现方式](#3. 防悬挂实现方式)
      • [Seata TCC 模式的实现方式](#Seata TCC 模式的实现方式)
        • [1. 幂等性实现](#1. 幂等性实现)
        • [2. 空回滚实现](#2. 空回滚实现)
        • [3. 防悬挂实现](#3. 防悬挂实现)
        • [Seata TCC 的实现机制](#Seata TCC 的实现机制)
        • [Seata TCC 的优势](#Seata TCC 的优势)
        • [Seata TCC vs 手动实现](#Seata TCC vs 手动实现)
    • [5. 分布式锁有几种实现方式?](#5. 分布式锁有几种实现方式?)
      • [1. 数据库实现](#1. 数据库实现)
      • [2. Redis SETNX](#2. Redis SETNX)
      • [3. Redisson](#3. Redisson)
      • [4. ZooKeeper](#4. ZooKeeper)
      • [5. etcd](#5. etcd)
    • [6. Redis 分布式锁怎么实现?](#6. Redis 分布式锁怎么实现?)
    • [7. RedLock 的原理和争议?](#7. RedLock 的原理和争议?)
    • [8. ZooKeeper 分布式锁的实现原理?](#8. ZooKeeper 分布式锁的实现原理?)
    • [9. 缓存和数据库一致性怎么保证?](#9. 缓存和数据库一致性怎么保证?)
    • [10. 缓存穿透、击穿、雪崩是什么?怎么解决?](#10. 缓存穿透、击穿、雪崩是什么?怎么解决?)
    • [11. 分布式 ID 怎么生成?](#11. 分布式 ID 怎么生成?)
    • [12. 雪花算法是怎么实现的?](#12. 雪花算法是怎么实现的?)
    • [13. 雪花算法的时钟回拨问题怎么解决?](#13. 雪花算法的时钟回拨问题怎么解决?)
    • [14. 消息队列如何保证消息不丢失?](#14. 消息队列如何保证消息不丢失?)
    • [15. 消息队列如何保证消息不重复消费?](#15. 消息队列如何保证消息不重复消费?)
    • [16. 消息队列如何保证消息顺序性?](#16. 消息队列如何保证消息顺序性?)
    • [17. 微服务之间如何保证事务?](#17. 微服务之间如何保证事务?)
    • [18. 什么是脑裂问题?怎么解决?](#18. 什么是脑裂问题?怎么解决?)
    • [19. 服务注册发现的原理?Nacos 和 Eureka 的区别?](#19. 服务注册发现的原理?Nacos 和 Eureka 的区别?)
    • [20. 熔断和限流的区别?Sentinel 怎么实现?](#20. 熔断和限流的区别?Sentinel 怎么实现?)
    • [21. 分库分表怎么做?有什么问题?](#21. 分库分表怎么做?有什么问题?)
    • [22. 如何设计一个高可用系统?](#22. 如何设计一个高可用系统?)
    • [23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?](#23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?)
      • [场景一:AB 事务提交后 C 才开始执行](#场景一:AB 事务提交后 C 才开始执行)
      • [场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交](#场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交)
      • [方案二:CountDownLatch + 编程式事务](#方案二:CountDownLatch + 编程式事务)
      • [方案三:CompletableFuture + 编程式事务](#方案三:CompletableFuture + 编程式事务)
      • 方案二:本地消息表(适用于场景一)
      • [方案三:消息队列 + 延迟检查](#方案三:消息队列 + 延迟检查)
      • 方案四:2PC(两阶段提交)
      • 方案对比
        • [场景一:AB 事务提交后 C 才开始执行](#场景一:AB 事务提交后 C 才开始执行)
        • [场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交](#场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交)
        • 场景选择

1. CAP 定理和 BASE 理论是什么?

回答:CAP 定理指分布式系统中一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者最多只能同时满足两个,由于网络分区不可避免,实际上是在 C 和 A 之间权衡。BASE 理论是 CAP 的延伸,包括基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventually Consistent),是对 CAP 中 AP 方案的补充,通过牺牲强一致性换取可用性,允许数据在一段时间内不一致,但最终达到一致状态。


2. 分布式事务有哪些实现方案?

回答:分布式事务主要有以下方案:

1. 2PC(两阶段提交)

实施描述

  • 阶段一(Prepare):协调者向所有参与者发送 prepare 请求,参与者执行事务但不提交,返回 Yes/No
  • 阶段二(Commit/Rollback):如果所有参与者都返回 Yes,协调者发送 commit;否则发送 rollback

代码示例

java 复制代码
// 协调者
public class TwoPhaseCommitCoordinator {
    public boolean executeTransaction(List<Participant> participants) {
        // 阶段一:准备阶段
        List<Boolean> prepareResults = new ArrayList<>();
        for (Participant p : participants) {
            prepareResults.add(p.prepare()); // 执行但不提交
        }
        
        // 阶段二:提交或回滚
        if (prepareResults.stream().allMatch(r -> r)) {
            participants.forEach(Participant::commit);
            return true;
        } else {
            participants.forEach(Participant::rollback);
            return false;
        }
    }
}

优缺点

  • 优点:强一致性,实现简单
  • 缺点:同步阻塞、单点故障、数据不一致风险(协调者宕机)

2. 3PC(三阶段提交)

实施描述

  • 阶段一(CanCommit):协调者询问参与者是否可以提交,参与者返回 Yes/No(不锁定资源)
  • 阶段二(PreCommit):如果都返回 Yes,发送 preCommit,参与者执行事务但不提交
  • 阶段三(DoCommit):发送 commit,参与者提交事务

改进点

  • 增加 CanCommit 阶段,提前发现不可提交的情况
  • 引入超时机制,参与者超时自动提交(假设协调者正常)

优缺点

  • 优点:减少阻塞时间,降低数据不一致风险
  • 缺点:实现复杂,仍存在数据不一致可能

3. TCC 模式(Try-Confirm-Cancel)

实施描述

  • Try:尝试执行,预留资源(如冻结库存、预扣余额)
  • Confirm:确认执行,真正扣减资源(如扣减库存、扣减余额)
  • Cancel:取消操作,释放预留资源(如解冻库存、退回余额)

代码示例

java 复制代码
// 库存服务
public class InventoryService {
    @TCC
    public boolean tryReserve(Long productId, Integer quantity) {
        // Try:冻结库存
        return inventoryMapper.freeze(productId, quantity) > 0;
    }
    
    public boolean confirmReserve(Long productId, Integer quantity) {
        // Confirm:扣减库存
        return inventoryMapper.deduct(productId, quantity) > 0;
    }
    
    public boolean cancelReserve(Long productId, Integer quantity) {
        // Cancel:解冻库存
        return inventoryMapper.unfreeze(productId, quantity) > 0;
    }
}

注意事项

  • 幂等性:三个方法都要保证幂等
  • 空回滚:Try 未执行时 Cancel 也要能执行
  • 悬挂问题:Cancel 先于 Try 执行的情况

优缺点

  • 优点:性能好(无全局锁)、最终一致性强
  • 缺点:业务侵入性强,需要实现三个接口

4. Saga 模式

实施描述

  • 将长事务拆分为多个本地事务(子事务)
  • 每个子事务都有对应的补偿操作
  • 如果某个子事务失败,执行前面所有子事务的补偿操作

两种实现方式

  1. 编排式(Orchestration):中央协调器统一调度
  2. 协同式(Choreography):各服务通过事件通信

代码示例

java 复制代码
// 订单服务
public class OrderSaga {
    public void createOrder(Order order) {
        // 1. 创建订单(本地事务)
        orderMapper.insert(order);
        
        // 2. 扣减库存(调用库存服务)
        inventoryService.deduct(order.getProductId(), order.getQuantity());
        
        // 3. 扣减余额(调用账户服务)
        accountService.deduct(order.getUserId(), order.getAmount());
    }
    
    // 补偿操作
    public void compensateOrder(Order order) {
        // 反向操作:取消订单、退回库存、退回余额
        orderMapper.cancel(order.getId());
        inventoryService.refund(order.getProductId(), order.getQuantity());
        accountService.refund(order.getUserId(), order.getAmount());
    }
}

优缺点

  • 优点:适合长事务、性能好
  • 缺点:补偿逻辑复杂,可能出现补偿失败

5. 本地消息表

实施描述

  1. 业务操作和消息写入同一本地事务
  2. 定时任务扫描消息表,发送未发送的消息
  3. 消息队列消费者处理消息,完成后回调确认
  4. 定时任务删除已确认的消息

事务执行逻辑

1. 本地事务执行(原子性保证)
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    /**
     * 创建订单 - 本地事务
     * 业务操作和消息写入在同一事务中,要么都成功,要么都失败
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        try {
            // 1. 创建订单(业务操作)
            orderMapper.insert(order);
            
            // 2. 写入本地消息表(消息记录)
            LocalMessage message = new LocalMessage();
            message.setMessageId(UUID.randomUUID().toString());
            message.setBusinessId(order.getId()); // 业务ID
            message.setContent(JSON.toJSONString(order));
            message.setStatus("PENDING"); // 待发送
            message.setRetryCount(0);
            message.setCreateTime(new Date());
            localMessageMapper.insert(message);
            
            // 如果这里抛出异常,整个事务会回滚
            // 订单和消息都不会写入数据库
            
        } catch (Exception e) {
            // 事务自动回滚(@Transactional)
            // 订单和消息都不会写入,保证一致性
            log.error("创建订单失败,事务回滚", e);
            throw e;
        }
    }
}

事务失败处理

  • 如果 orderMapper.insert() 失败 → 整个事务回滚,消息不会写入
  • 如果 localMessageMapper.insert() 失败 → 整个事务回滚,订单不会写入
  • 如果任何步骤抛异常 → @Transactional 自动回滚,数据库恢复到事务前状态
2. 消息发送(定时任务)
java 复制代码
@Component
public class MessageSender {
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 定时任务:扫描待发送消息并发送
     */
    @Scheduled(fixedDelay = 5000) // 每5秒执行一次
    public void sendPendingMessages() {
        // 查询待发送的消息(状态为 PENDING)
        List<LocalMessage> messages = localMessageMapper.selectByStatus("PENDING");
        
        for (LocalMessage msg : messages) {
            try {
                // 发送到消息队列
                SendResult result = rocketMQTemplate.syncSend(
                    "order-topic", 
                    MessageBuilder.withPayload(msg.getContent()).build()
                );
                
                if (result.getSendStatus() == SendStatus.SEND_OK) {
                    // 发送成功,更新状态为已发送
                    msg.setStatus("SENT");
                    msg.setSendTime(new Date());
                    localMessageMapper.update(msg);
                }
                
            } catch (Exception e) {
                // 发送失败,增加重试次数
                msg.setRetryCount(msg.getRetryCount() + 1);
                
                // 超过最大重试次数,标记为失败
                if (msg.getRetryCount() >= 3) {
                    msg.setStatus("FAILED");
                    // 可以发送告警或记录日志
                    log.error("消息发送失败,超过最大重试次数: {}", msg.getMessageId());
                }
                
                localMessageMapper.update(msg);
            }
        }
    }
}
3. 消费者处理(最终一致性)
java 复制代码
@Component
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-consumer-group"
)
public class OrderConsumer implements RocketMQListener<String> {
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private AccountService accountService;
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    /**
     * 消费消息:扣减库存和余额
     */
    @Override
    public void onMessage(String messageContent) {
        try {
            // 1. 解析消息
            Order order = JSON.parseObject(messageContent, Order.class);
            
            // 2. 幂等性检查:查询消息是否已处理
            LocalMessage msg = localMessageMapper.selectByBusinessId(order.getId());
            if (msg != null && "CONSUMED".equals(msg.getStatus())) {
                log.info("消息已处理,跳过: {}", order.getId());
                return; // 已处理,直接返回
            }
            
            // 3. 处理业务逻辑
            inventoryService.deduct(order.getProductId(), order.getQuantity());
            accountService.deduct(order.getUserId(), order.getAmount());
            
            // 4. 更新消息状态为已消费
            msg.setStatus("CONSUMED");
            msg.setConsumeTime(new Date());
            localMessageMapper.update(msg);
            
        } catch (Exception e) {
            log.error("消费消息失败", e);
            // 消费失败,消息会重新投递(RocketMQ 自动重试)
            // 或者可以记录到死信队列,人工处理
            throw e; // 抛出异常,触发重试
        }
    }
}
4. 补偿机制(处理失败场景)
java 复制代码
@Component
public class CompensationHandler {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    /**
     * 定时任务:处理失败的消息
     * 1. 消息发送失败超过3次 → 人工介入或补偿
     * 2. 消息消费失败超过重试次数 → 回滚本地业务
     */
    @Scheduled(fixedDelay = 60000) // 每分钟执行一次
    public void handleFailedMessages() {
        // 查询发送失败的消息
        List<LocalMessage> failedMessages = localMessageMapper.selectByStatus("FAILED");
        
        for (LocalMessage msg : failedMessages) {
            try {
                // 根据业务类型执行补偿
                Order order = JSON.parseObject(msg.getContent(), Order.class);
                
                // 补偿逻辑:取消订单
                orderMapper.cancelOrder(order.getId());
                
                // 更新消息状态为已补偿
                msg.setStatus("COMPENSATED");
                localMessageMapper.update(msg);
                
            } catch (Exception e) {
                log.error("补偿处理失败: {}", msg.getMessageId(), e);
            }
        }
    }
    
    /**
     * 死信队列消费者:处理消费失败的消息
     */
    @RocketMQMessageListener(
        topic = "order-topic-dlq", // 死信队列
        consumerGroup = "dlq-consumer-group"
    )
    public class DeadLetterConsumer implements RocketMQListener<String> {
        @Override
        public void onMessage(String messageContent) {
            // 解析消息,执行补偿或告警
            Order order = JSON.parseObject(messageContent, Order.class);
            
            // 1. 回滚本地业务(取消订单)
            orderMapper.cancelOrder(order.getId());
            
            // 2. 发送告警通知
            alertService.sendAlert("订单处理失败,已回滚: " + order.getId());
            
            // 3. 记录日志,便于排查
            log.error("死信队列消息,已执行补偿: {}", order.getId());
        }
    }
}

完整流程图

复制代码
1. 创建订单(本地事务)
   ├─ 插入订单表
   ├─ 插入消息表(状态:PENDING)
   └─ 事务提交 ✅ 或 回滚 ❌

2. 定时任务发送消息
   ├─ 查询 PENDING 状态消息
   ├─ 发送到消息队列
   └─ 更新状态为 SENT ✅ 或 重试/失败 ❌

3. 消费者处理消息
   ├─ 幂等性检查
   ├─ 扣减库存和余额
   └─ 更新状态为 CONSUMED ✅ 或 重试 ❌

4. 补偿机制
   ├─ 发送失败 → 人工介入或自动补偿
   └─ 消费失败 → 死信队列 → 回滚本地业务

关键点总结

  1. 本地事务保证原子性:业务操作和消息写入在同一事务,失败时自动回滚
  2. 消息发送失败处理:重试机制,超过次数标记失败,触发补偿
  3. 消费失败处理:RocketMQ 自动重试,最终失败进入死信队列,执行补偿
  4. 幂等性保证:通过消息状态和业务ID判断是否已处理
  5. 最终一致性:通过重试和补偿机制保证最终一致

优缺点

  • 优点:实现简单、最终一致、不依赖第三方事务管理器、支持补偿
  • 缺点:需要维护消息表、有延迟、可能出现消息丢失(需要监控和告警)

6. Seata 框架

实施描述

Seata 是阿里开源的分布式事务框架,支持四种模式:

  1. AT 模式:无侵入,自动生成回滚 SQL,适合大多数场景
  2. TCC 模式:需要实现 Try/Confirm/Cancel 接口,性能好
  3. Saga 模式:长事务,通过状态机编排
  4. XA 模式:基于 XA 协议,强一致但性能差

AT 模式使用示例

java 复制代码
@GlobalTransactional
public void createOrder(Order order) {
    // 1. 创建订单(本地事务)
    orderMapper.insert(order);
    
    // 2. 扣减库存(远程调用,自动参与分布式事务)
    inventoryService.deduct(order.getProductId(), order.getQuantity());
    
    // 3. 扣减余额(远程调用,自动参与分布式事务)
    accountService.deduct(order.getUserId(), order.getAmount());
}

核心组件

  • TC(Transaction Coordinator):事务协调器,管理全局事务
  • TM(Transaction Manager):事务管理器,开启/提交/回滚全局事务
  • RM(Resource Manager):资源管理器,管理分支事务

优缺点

  • 优点:AT 模式无侵入、支持多种模式、社区活跃
  • 缺点:需要部署 TC 服务、AT 模式有性能损耗

选择建议

  • 一般场景:Seata AT 模式(无侵入,易用)
  • 高并发场景:TCC 模式(性能好,无全局锁)
  • 允许延迟场景:本地消息表 + 消息队列(最终一致,解耦)
  • 长事务场景:Saga 模式(适合业务流程长的场景)

3. Seata 的 AT 模式原理是什么?

回答:Seata AT 模式是一种无侵入的分布式事务方案。原理是:

  1. 一阶段:业务 SQL 执行前,记录数据快照(beforeImage);执行后记录 afterImage;生成行锁,提交本地事务
  2. 二阶段提交:删除 undo log 和行锁,异步完成
  3. 二阶段回滚:根据 beforeImage 生成反向 SQL,恢复数据,删除 undo log

核心组件:TC(事务协调器)、TM(事务管理器)、RM(资源管理器)。AT 模式对业务无侵入,只需加 @GlobalTransactional 注解,适合大多数业务场景。


4. TCC 模式的原理和适用场景?

回答:TCC 是 Try-Confirm-Cancel 的缩写:

  • Try:预留资源,如冻结库存、预扣余额
  • Confirm:确认执行,真正扣减资源
  • Cancel:取消操作,释放预留资源

特点:业务侵入性强,需要实现三个接口;性能好,无全局锁;适合高并发、资金类业务。

需要注意:幂等性(重复调用结果一致)、空回滚(Try 未执行时的 Cancel)、悬挂问题(Cancel 先于 Try 执行)。


4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?

总结 :TCC 模式需要解决三个核心问题:1. 幂等性 :通过状态记录、唯一索引、Redis 去重等方式实现;2. 空回滚 :通过检查 Try 记录、状态标记、时间窗口等方式实现;3. 防悬挂:通过状态检查、时间戳、分布式锁等方式实现。主要实现方式包括:状态表记录、Redis 缓存、数据库唯一索引、时间戳判断等。

回答

问题场景

在 TCC 分布式事务中,由于网络延迟、重试机制等原因,可能出现以下问题:

  1. 幂等性问题:同一个方法被重复调用,需要保证结果一致
  2. 空回滚问题:Try 未执行,但 Cancel 先执行
  3. 悬挂问题:Cancel 先于 Try 执行,导致 Try 无法执行
  4. 并发问题:多个线程同时执行同一个方法,可能导致重复执行

并发控制方案

  1. 数据库行锁(SELECT FOR UPDATE,推荐)

    • 在事务中使用 SELECT FOR UPDATE 加行锁
    • 保证同一时间只有一个线程能执行
    • 性能好,实现简单
  2. 分布式锁(Redis/Redisson)

    • 使用分布式锁保证全局唯一
    • 适合多实例部署场景
    • 性能略低于数据库锁
  3. 唯一索引 + 异常处理

    • 利用数据库唯一索引防止重复插入
    • 如果插入失败,说明已执行过
    • 适合简单场景

解决方案列表

1. 幂等性实现方式

核心原理:通过记录执行状态,重复调用时检查状态,已执行则直接返回。

实现方式

方式1:状态表记录 + 唯一索引 + SELECT FOR UPDATE(推荐)

java 复制代码
// 创建事务记录表(branch_id 需要唯一索引)
CREATE TABLE tcc_transaction (
    tx_id VARCHAR(64) PRIMARY KEY,
    branch_id VARCHAR(64) UNIQUE,  -- 唯一索引,防止并发插入
    status TINYINT,  -- 状态:-1:Try占位, 0:Try已执行, 1:Confirm已执行, 2:Cancel已执行
    create_time BIGINT,
    update_time BIGINT,
    INDEX idx_branch_status (branch_id, status)
);

@Service
public class InventoryService {
    
    @Autowired
    private TccTransactionMapper tccTransactionMapper;
    
    // Try 方法(幂等 + 并发控制)
    @Transactional
    public boolean tryReserve(String txId, String branchId, Long productId, Integer quantity) {
        try {
            // 1. 插入占位记录(唯一索引防止并发插入)
            tccTransactionMapper.insertPlaceholder(txId, branchId, -1);
        } catch (DuplicateKeyException e) {
            // 记录已存在,继续执行查询逻辑
        }
        
        // 2. 加行锁查询(SELECT FOR UPDATE 防止并发更新)
        TccTransaction record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
        
        // 3. 检查状态(幂等)
        if (record.getStatus() == 0) {
            return true;  // 已执行过 Try
        }
        if (record.getStatus() == 2) {
            throw new RuntimeException("Cancel 已执行,Try 不能执行");  // 防悬挂
        }
        
        // 4. 执行 Try 操作并更新状态
        boolean result = inventoryMapper.freeze(productId, quantity) > 0;
        if (result) {
            tccTransactionMapper.updateStatus(branchId, 0);
        } else {
            tccTransactionMapper.deleteByBranchId(branchId);
        }
        return result;
    }
    
    // Confirm 方法(幂等)
    @Transactional
    public boolean confirmReserve(String txId, String branchId, Long productId, Integer quantity) {
        TccTransaction record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
        
        if (record.getStatus() == 1) {
            return true;  // 已执行过 Confirm(幂等)
        }
        if (record.getStatus() != 0) {
            throw new RuntimeException("Try 未执行或执行失败");
        }
        
        boolean result = inventoryMapper.deduct(productId, quantity) > 0;
        if (result) {
            tccTransactionMapper.updateStatus(branchId, 1);
        }
        return result;
    }
    
    // Cancel 方法(幂等 + 空回滚)
    // 
    // 关键理解:Cancel 阶段判断 Try 阶段插入的记录是否存在,如果不存在,则不执行回滚操作
    // 
    // 场景1:Cancel 先执行,Try 未执行(记录不存在)
    // T1: Cancel 执行 → 查询记录 → 不存在 → 直接返回(不执行回滚)
    // T2: Try 执行 → 插入占位记录(status=-1)→ 执行 Try 操作 → 更新为 status=0
    // 说明:Cancel 不执行回滚,因为 Try 记录不存在,没有需要回滚的资源
    // 
    // 场景2:Try 占位后,Cancel 执行(记录存在,status=-1)
    // T1: Try 执行 → 插入占位记录(status=-1)→ 执行 Try 操作...
    // T2: Cancel 执行 → 查询记录 → status=-1 → 不执行回滚(Try 未执行,无资源需要回滚)
    // T3: Try 执行完成 → 更新为 status=0
    // 说明:Cancel 不执行回滚,因为 Try 操作可能还未完成,没有资源需要回滚
    // 
    // 场景3:Try 已执行,Cancel 执行(正常回滚)
    // T1: Try 执行 → 更新状态为 0 → 执行 Try 操作成功(冻结资源)
    // T2: Cancel 执行 → 查询记录 → status=0 → 执行解冻 → 更新为 status=2
    // 说明:Cancel 执行回滚,因为 Try 已执行,需要解冻资源
    // 
    // 场景4:Cancel 已执行(幂等)
    // T1: Cancel 执行 → 查询记录 → status=2 → 幂等返回
    @Transactional
    public boolean cancelReserve(String txId, String branchId, Long productId, Integer quantity) {
        // 1. 先查询记录是否存在(不加锁,用于判断是否需要回滚)
        TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
        
        // 2. 如果记录不存在,说明 Try 未执行,不执行回滚操作
        if (record == null) {
            return true;  // Try 未执行,无需回滚
        }
        
        // 3. 加行锁查询(如果其他线程正在执行 Cancel,会等待锁释放)
        record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
        
        // 4. 检查状态(幂等)
        if (record.getStatus() == 2) {
            // 已执行过 Cancel,幂等返回
            return true;
        }
        
        // 5. 执行 Cancel 操作
        boolean result = true;
        if (record.getStatus() == 0) {
            // Try 已执行,需要解冻(正常回滚)
            result = inventoryMapper.unfreeze(productId, quantity) > 0;
        } else if (record.getStatus() == -1) {
            // Try 占位状态,Try 未执行,不执行回滚操作
            // 说明:Try 可能正在执行或未执行,没有资源需要回滚
            result = true;  // 无需回滚,直接返回成功
        }
        
        // 6. 更新状态为 Cancel(只有 Try 已执行时才更新状态)
        if (result) {
            if (record.getStatus() == 0) {
                // Try 已执行,更新状态为 Cancel
                tccTransactionMapper.updateStatus(branchId, 2);
            }
            // 如果 status == -1,不更新状态,保持 Try 占位状态
            // 如果 status == 2,已经在步骤4中幂等返回了
        } else {
            // Cancel 失败,删除记录(只有业务操作失败时才删除)
            tccTransactionMapper.deleteByBranchId(branchId);
        }
        
        return result;
    }
}

@Mapper
public interface TccTransactionMapper {
    @Select("SELECT * FROM tcc_transaction WHERE branch_id = #{branchId}")
    TccTransaction selectByBranchId(String branchId);
    
    @Select("SELECT * FROM tcc_transaction WHERE branch_id = #{branchId} FOR UPDATE")
    TccTransaction selectByBranchIdForUpdate(String branchId);
    
    @Insert("INSERT INTO tcc_transaction (tx_id, branch_id, status, create_time) VALUES (#{txId}, #{branchId}, #{status}, #{createTime})")
    void insertPlaceholder(@Param("txId") String txId, @Param("branchId") String branchId, @Param("status") int status);
    
    @Update("UPDATE tcc_transaction SET status = #{status} WHERE branch_id = #{branchId}")
    void updateStatus(@Param("branchId") String branchId, @Param("status") int status);
    
    @Delete("DELETE FROM tcc_transaction WHERE branch_id = #{branchId}")
    void deleteByBranchId(String branchId);
}

其他实现方式

方式2:Redis 去重(使用 SETNX 原子操作)

java 复制代码
// 使用 SETNX 原子操作,防止并发
Boolean setResult = redisTemplate.opsForValue().setIfAbsent(key, "1", 1, TimeUnit.HOURS);
if (Boolean.FALSE.equals(setResult)) {
    return true;  // 已执行过(幂等)
}
// 执行操作...

方式3:数据库唯一索引

java 复制代码
// 利用唯一索引防止重复插入
try {
    tccTryRecordMapper.insert(new TccTryRecord(txId, branchId));
    // 执行操作...
} catch (DuplicateKeyException e) {
    return true;  // 已执行过(幂等)
}

方式4:分布式锁

java 复制代码
// 使用 Redisson 分布式锁
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
    // 检查状态并执行操作
}

2. 空回滚实现方式

核心原理:Cancel 阶段判断 Try 阶段插入的记录是否存在,如果不存在,则不执行回滚操作。

实现方式

方式1:状态表记录(推荐)

在 Cancel 方法中,先查询 Try 记录是否存在:

  • 如果记录不存在:说明 Try 未执行,无需回滚,直接返回
  • 如果记录存在且 status=0:说明 Try 已执行,执行解冻操作(正常回滚)
  • 如果记录存在且 status=-1:说明 Try 占位但未执行,无需回滚,直接返回

详见上面的 Cancel 方法实现。

方式2:Redis 去重

java 复制代码
// Cancel 方法中,检查 Redis 中是否存在 Try 记录
String tryKey = "tcc:try:" + branchId;
if (!redisTemplate.hasKey(tryKey)) {
    return true;  // Try 未执行,空回滚
}
// 执行 Cancel 操作...

方式3:分布式锁 + 状态检查

java 复制代码
// 使用分布式锁保证并发安全,同时检查状态
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
    try {
        // 检查 Try 记录是否存在
        TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
        if (record == null) {
            return true;  // Try 未执行,空回滚
        }
        // 执行 Cancel 操作...
    } finally {
        lock.unlock();
    }
}

3. 防悬挂实现方式

核心原理:Try 执行前检查 Cancel 是否已执行,如果已执行则抛出异常。

实现方式

方式1:状态表记录(推荐)

在 Try 方法中,查询记录状态,如果 status=2(Cancel 已执行),抛出异常。详见上面的 Try 方法实现。

方式2:Redis 去重

java 复制代码
// Try 方法中,检查 Redis 中是否存在 Cancel 记录
String cancelKey = "tcc:cancel:" + branchId;
if (redisTemplate.hasKey(cancelKey)) {
    throw new RuntimeException("Cancel 已执行,Try 不能执行");  // 防悬挂
}
// 执行 Try 操作...

方式3:分布式锁 + 状态检查

java 复制代码
// 使用分布式锁保证并发安全,同时检查状态
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
    try {
        // 检查 Cancel 是否已执行
        TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
        if (record != null && record.getStatus() == 2) {
            throw new RuntimeException("Cancel 已执行,Try 不能执行");  // 防悬挂
        }
        // 执行 Try 操作...
    } finally {
        lock.unlock();
    }
}

重要说明

  • 状态表记录:可靠性高,可追溯,适合生产环境(与 Seata 的实现方式类似)
  • Redis 去重:性能好,但数据可能丢失,需要持久化机制
  • 分布式锁:主要用于并发控制,需要配合状态检查才能实现空回滚和防悬挂
  • Seata 实现 :基于 tcc_fence_log 表(类似于状态表记录),框架自动处理,无需手动实现

⚠️ 重要问题:SELECT FOR UPDATE 在记录不存在时的问题

java 复制代码
// 问题:SELECT FOR UPDATE 只能锁定已存在的行,不能锁定不存在的行
// 解决方案:先插入占位记录(唯一索引),再使用 SELECT FOR UPDATE 查询

关键点说明

  1. SELECT FOR UPDATE 的限制

    • ⚠️ 记录不存在时不会加锁,必须先插入占位记录(唯一索引),再查询
  2. 唯一索引 vs SELECT FOR UPDATE

    • 唯一索引:防止并发插入,保证只有一个线程能插入成功
    • SELECT FOR UPDATE:防止并发更新,保证查询和更新的原子性
    • 两者配合:唯一索引保证插入安全,SELECT FOR UPDATE 保证更新安全
  3. 执行流程

    复制代码
    线程1:插入占位记录(唯一索引)→ SELECT FOR UPDATE(获得锁)→ 执行操作 → 更新状态
    线程2:插入失败(唯一索引)→ SELECT FOR UPDATE(等待锁)→ 获得锁后检查状态(幂等)

实现方式对比

实现方式 优点 缺点 并发控制 适用场景
状态表记录 + 行锁 可靠性高、可追溯、支持复杂状态、并发安全 需要数据库、性能略低 SELECT FOR UPDATE 生产环境推荐
状态表记录(无锁) 可靠性高、可追溯 并发不安全、可能重复执行 ❌ 无 不推荐
Redis 去重 性能好、实现简单 数据可能丢失、需要持久化 Redis 原子操作 高并发场景
唯一索引 实现简单、数据库保证 只能防重复插入 数据库唯一约束 简单场景
分布式锁 强一致性、多实例安全 性能开销、可能死锁 Redis/Redisson 多实例部署
时间窗口 灵活、可配置 时间判断不准确、并发不安全 ❌ 无 不推荐

并发控制方式对比

并发控制方式 实现方式 优点 缺点 推荐度
数据库行锁 SELECT FOR UPDATE 可靠性高、实现简单、性能好 需要事务、单实例 ⭐⭐⭐⭐⭐
分布式锁 Redis/Redisson 多实例安全、功能完善 性能开销、依赖Redis ⭐⭐⭐⭐
唯一索引 数据库唯一约束 实现简单、数据库保证 只能防插入、不能防更新 ⭐⭐⭐
无锁 性能最好 并发不安全、可能重复执行 ❌ 不推荐

最佳实践

  1. 推荐方案:唯一索引 + SELECT FOR UPDATE 行锁

    • 可靠性高,支持幂等、空回滚、防悬挂
    • 并发安全,通过唯一索引 + 行锁保证原子性
    • 可追溯,便于排查问题
    • 必须配合使用
      • 唯一索引:防止并发插入
      • SELECT FOR UPDATE:防止并发更新
  2. 并发控制(重要!)

    • 唯一索引的作用:防止并发插入,保证只有一个线程能插入成功
    • SELECT FOR UPDATE 的作用:防止并发更新,保证查询和更新的原子性
    • 为什么两者都需要
      • 唯一索引只能防止插入,不能防止更新
      • 如果只使用唯一索引,多个线程可能同时更新同一条记录的状态
      • SELECT FOR UPDATE 在查询和更新之间加锁,保证原子性
    • 实现方式
      • 先插入占位记录(利用唯一索引防止并发插入)
      • 然后使用 SELECT FOR UPDATE 查询(此时记录一定存在)
      • 在锁的保护下检查状态和更新状态
    • 事务保证 :使用 @Transactional 确保整个方法在事务中执行
    • 锁的范围:只锁定当前分支事务的记录,不影响其他分支
    • 多实例部署:如果多实例部署,考虑使用分布式锁(Redis/Redisson)
  3. 性能优化

    • 状态表添加索引:(branch_id, status)
    • 使用 Redis 缓存热点数据
    • 批量处理状态更新
    • 行锁只锁定必要的数据,减少锁竞争
  4. 监控告警

    • 监控空回滚次数
    • 监控悬挂情况
    • 监控幂等调用次数
    • 监控并发冲突次数(锁等待时间)

Seata TCC 模式的实现方式

Seata 在 1.5.1 版本引入了 tcc_fence_log 表,通过状态记录机制解决幂等、悬挂和空回滚问题。

1. 幂等性实现

核心原理 :通过 tcc_fence_log 表记录每个事务分支的执行状态,查询该表判断某个阶段是否已执行。

实现方式

sql 复制代码
-- Seata 自动创建 tcc_fence_log 表
CREATE TABLE tcc_fence_log (
    xid VARCHAR(128) NOT NULL,
    branch_id BIGINT NOT NULL,
    action_name VARCHAR(64) NOT NULL,
    status TINYINT NOT NULL,  -- 0:Committed, 1:Rollbacked, 2:Suspended
    gmt_create DATETIME(3) NOT NULL,
    gmt_modified DATETIME(3) NOT NULL,
    PRIMARY KEY (xid, branch_id, action_name),
    KEY idx_gmt_modified (gmt_modified),
    KEY idx_status (status)
);

工作流程

  1. Try 阶段:插入记录,status=0(Committed)
  2. Confirm 阶段:查询记录,如果 status=0 则执行 Confirm,更新状态
  3. Cancel 阶段:查询记录,如果 status=0 则执行 Cancel,更新 status=1(Rollbacked)

代码示例(Seata 自动处理):

java 复制代码
@LocalTCC  // 标识这是一个 TCC 接口
public interface InventoryService {
    @TwoPhaseBusinessAction(
        name = "inventoryService",  // 事务名称
        commitMethod = "confirm",   // Confirm 方法名
        rollbackMethod = "cancel",  // Cancel 方法名
        useTCCFence = true          // 启用 TCC Fence 机制(1.5.1+ 版本)
    )
    boolean tryReserve(BusinessActionContext context, Long productId, Integer quantity);
    
    boolean confirm(BusinessActionContext context);
    
    boolean cancel(BusinessActionContext context);
}

注解说明

  • @LocalTCC:标识这是一个 TCC 接口,框架会自动处理 TCC 相关逻辑
  • @TwoPhaseBusinessAction :标识 Try 方法,指定 Confirm 和 Cancel 方法名
    • name:事务名称,唯一标识
    • commitMethod:Confirm 方法名
    • rollbackMethod:Cancel 方法名
    • useTCCFence是否启用 TCC Fence 机制 (Seata 1.5.1+ 版本)
      • true:启用 TCC Fence,框架自动处理幂等、空回滚、防悬挂(推荐)
      • false:不启用 TCC Fence,需要业务代码手动处理幂等、空回滚、防悬挂
  • TCC Fence 机制 :当 useTCCFence = true 时,Seata 框架通过 AOP/拦截器自动处理幂等、空回滚、防悬挂
    • 框架会自动操作 tcc_fence_log
    • 框架会在 Try/Confirm/Cancel 方法执行前后自动插入/查询/更新状态记录
    • 业务代码只需实现业务逻辑,无需关心幂等、空回滚、防悬挂的处理

重要说明

  • Seata 1.5.1+ 版本 :引入了 useTCCFence 参数,默认值为 false(为了兼容旧版本)
  • 推荐设置useTCCFence = true,让框架自动处理幂等、空回滚、防悬挂
  • 数据库要求 :启用 TCC Fence 需要创建 tcc_fence_log 表(框架会自动创建,或手动创建)
2. 空回滚实现

核心原理 :Cancel 执行前检查 tcc_fence_log 表中是否存在对应的 Try 记录,如果不存在,说明是空回滚,直接返回成功。

实现方式

java 复制代码
// Seata 框架自动处理空回滚
// 1. Cancel 执行前,查询 tcc_fence_log 表
// 2. 如果记录不存在(Try 未执行),直接返回成功(空回滚)
// 3. 如果记录存在且 status=0,执行 Cancel 操作,更新 status=1

工作流程

复制代码
Cancel 执行:
  ↓
查询 tcc_fence_log 表(xid, branch_id, action_name)
  ↓
记录不存在?
  ├─ 是 → 空回滚,直接返回成功 ✅
  └─ 否 → 检查 status
      ├─ status=0 → 执行 Cancel 操作,更新 status=1
      └─ status=1 → 幂等返回(已执行过 Cancel)
3. 防悬挂实现

核心原理 :Cancel 或 Confirm 执行时,如果 Try 记录不存在,插入一条 status=2(Suspended)的记录,阻止后续的 Try 操作执行。

实现方式

java 复制代码
// Seata 框架自动处理防悬挂
// 1. Cancel/Confirm 执行时,如果 Try 记录不存在
// 2. 插入一条 status=2(Suspended)的记录
// 3. Try 后续执行时,查询到 status=2,抛出异常(防悬挂)

工作流程

复制代码
场景:Cancel 先执行,Try 未执行
  ↓
Cancel 执行:
  ↓
查询 tcc_fence_log 表(Try 记录)
  ↓
记录不存在?
  ├─ 是 → 插入 status=2(Suspended)记录,空回滚成功
  └─ 否 → 执行 Cancel 操作
  ↓
Try 后续执行:
  ↓
查询 tcc_fence_log 表
  ↓
status=2(Suspended)?
  ├─ 是 → 抛出异常(防悬挂)✅
  └─ 否 → 正常执行 Try
Seata TCC 的实现机制

useTCCFence 参数说明

  • 作用:控制是否启用 TCC Fence 机制,自动处理幂等、空回滚、防悬挂
  • 版本要求:Seata 1.5.1+ 版本支持
  • 默认值false(为了兼容旧版本,需要显式设置为 true 才能启用)
  • 推荐设置useTCCFence = true,让框架自动处理,减少业务代码复杂度

框架自动处理机制 (当 useTCCFence = true 时):

  1. AOP/拦截器机制

    • Seata 框架通过 AOP 或方法拦截器,在 Try/Confirm/Cancel 方法执行前后自动处理
    • 框架会自动操作 tcc_fence_log 表,业务代码无需关心
  2. Try 方法执行流程

    java 复制代码
    // 框架自动处理流程
    1. 方法执行前:查询 tcc_fence_log 表,检查是否已执行或已悬挂
    2. 如果 status=2(Suspended),抛出异常(防悬挂)
    3. 如果 status=0(Committed),直接返回(幂等)
    4. 执行业务逻辑(Try 方法)
    5. 方法执行后:插入/更新 tcc_fence_log 表,status=0(Committed)
  3. Confirm 方法执行流程

    java 复制代码
    // 框架自动处理流程
    1. 方法执行前:查询 tcc_fence_log 表,检查状态
    2. 如果记录不存在,插入 status=2(Suspended)记录(防悬挂)
    3. 如果 status=1(Rollbacked),抛出异常(已回滚,不能提交)
    4. 如果 status=2(Suspended),抛出异常(已悬挂,不能提交)
    5. 如果已执行过 Confirm,直接返回(幂等)
    6. 执行业务逻辑(Confirm 方法)
    7. 方法执行后:更新 tcc_fence_log 表状态
  4. Cancel 方法执行流程

    java 复制代码
    // 框架自动处理流程
    1. 方法执行前:查询 tcc_fence_log 表,检查状态
    2. 如果记录不存在,插入 status=2(Suspended)记录,直接返回(空回滚)
    3. 如果 status=1(Rollbacked),直接返回(幂等)
    4. 如果 status=2(Suspended),直接返回(空回滚)
    5. 如果 status=0(Committed),执行业务逻辑(Cancel 方法)
    6. 方法执行后:更新 tcc_fence_log 表,status=1(Rollbacked)

关键点

  • 必须设置 useTCCFence = true :在 @TwoPhaseBusinessAction 注解中显式设置,才能启用 TCC Fence 机制
  • 无需额外注解 :只需使用 @LocalTCC@TwoPhaseBusinessAction 定义接口
  • 框架自动处理 :当 useTCCFence = true 时,幂等、空回滚、防悬挂由框架自动处理,业务代码无需关心
  • 透明化:业务代码只需实现业务逻辑,框架自动处理状态管理
  • 数据库要求 :启用 TCC Fence 需要 tcc_fence_log 表(框架会自动创建,或手动创建)
Seata TCC 的优势
  1. 自动处理:框架自动处理幂等、空回滚、防悬挂,业务代码无需关心
  2. 统一管理 :通过 tcc_fence_log 表统一管理所有 TCC 分支事务状态
  3. 可靠性高:基于数据库事务,保证状态记录的一致性
  4. 易于排查 :通过 tcc_fence_log 表可以追溯所有事务分支的执行历史
  5. 代码简洁:只需使用注解定义接口,无需手动实现状态管理逻辑
Seata TCC vs 手动实现
对比项 Seata TCC 手动实现
幂等性 框架自动处理(基于 tcc_fence_log 表) 多种方式:状态表记录、Redis 去重、唯一索引、分布式锁
空回滚 框架自动处理(基于 tcc_fence_log 表) 多种方式:状态表记录、Redis 去重、分布式锁+状态检查
防悬挂 框架自动处理(基于 tcc_fence_log 表) 多种方式:状态表记录、Redis 去重、分布式锁+状态检查
实现方式 统一使用 tcc_fence_log 可选择:状态表、Redis、分布式锁等
代码侵入 只需实现业务接口 需要实现完整的状态管理逻辑
可靠性 框架保证(基于数据库事务) 需要自己保证并发安全和数据一致性
维护成本 高(需要维护多种实现方式)

手动实现的多种方式对比

实现方式 幂等性 空回滚 防悬挂 并发控制 可靠性 推荐度
状态表记录 + 行锁 SELECT FOR UPDATE ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Redis 去重 Redis 原子操作 ⭐⭐⭐ ⭐⭐⭐
分布式锁 + 状态检查 Redis/Redisson ⭐⭐⭐⭐ ⭐⭐⭐⭐
唯一索引 数据库唯一约束 ⭐⭐⭐ ⭐⭐

使用建议

  • 推荐使用 Seata TCC :框架自动处理幂等、空回滚、防悬挂,减少业务代码复杂度,统一使用 tcc_fence_log
  • 手动实现 :只有在特殊场景下(如无法使用 Seata)才考虑手动实现
    • 首选:状态表记录 + 行锁(与 Seata 实现方式类似,可靠性高)
    • 高并发场景:Redis 去重(性能好,但需要持久化机制)
    • 多实例部署:分布式锁 + 状态检查(多实例安全)

5. 分布式锁有几种实现方式?

回答:分布式锁主要有以下几种实现方式:

实现方式 原理 优点 缺点 适用场景
数据库 基于唯一索引或乐观锁 实现简单、无需额外组件 性能差、有死锁风险 并发量低、简单场景
Redis SETNX + 过期时间 性能好、实现简单 主从切换可能丢锁 高并发、最终一致可接受
Redisson Redis + 看门狗机制 自动续期、功能完善 依赖 Redis 生产环境推荐
RedLock 多 Redis 节点投票 提高可靠性 仍有争议、实现复杂 对可靠性要求高
ZooKeeper 临时顺序节点 强一致、自动释放 性能较低、需维护 ZK 强一致、可靠性要求高
etcd Lease + Revision 强一致、性能好 需要维护 etcd 云原生、K8s 环境

详细说明

1. 数据库实现

基于唯一索引或乐观锁实现,适合并发量低的场景。

sql 复制代码
// 基于唯一索引
CREATE TABLE distributed_lock (
    lock_key VARCHAR(64) PRIMARY KEY,
    lock_value VARCHAR(64),
    expire_time BIGINT,
    INDEX idx_expire_time (expire_time)
);

// 加锁
INSERT INTO distributed_lock (lock_key, lock_value, expire_time) 
VALUES ('order:123', 'uuid', UNIX_TIMESTAMP() + 30)
ON DUPLICATE KEY UPDATE lock_value = 'uuid', expire_time = UNIX_TIMESTAMP() + 30;

// 解锁
DELETE FROM distributed_lock WHERE lock_key = 'order:123' AND lock_value = 'uuid';

2. Redis SETNX

使用 SET key value NX EX 30 实现,性能好但主从切换可能丢锁。

java 复制代码
// 加锁
String result = jedis.set(key, value, "NX", "EX", 30);

// 解锁(Lua 脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

3. Redisson

封装了加锁、续期、解锁逻辑,支持看门狗自动续期,生产环境推荐。

java 复制代码
RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

4. ZooKeeper

基于临时顺序节点实现,强一致性但性能较低。

java 复制代码
// 创建临时顺序节点
String lockPath = zkClient.create("/locks/order-", data, CreateMode.EPHEMERAL_SEQUENTIAL);
// 判断是否是最小节点,获取锁

5. etcd

基于 Lease(租约)和 Revision(版本号)实现,强一致且性能好,适合云原生环境。

核心机制

  • Lease(租约):为 key 设置过期时间,类似 Redis 的过期机制
  • Revision(版本号):etcd 中每个 key 都有全局递增的版本号,用于实现公平锁
  • Watch(监听):监听 key 变化,实现阻塞等待
  • Prefix(前缀):通过前缀查询,实现锁队列

etcd 分布式锁的优势

  1. 强一致性:基于 Raft 算法,保证强一致
  2. 自动释放:Lease 过期自动释放,防止死锁
  3. 公平锁:通过 Revision 实现 FIFO 队列
  4. 性能好:比 ZooKeeper 性能更好
  5. Watch 机制:支持阻塞等待,无需轮询

与 Redis 和 ZooKeeper 的对比

特性 etcd Redis ZooKeeper
一致性 强一致(Raft) 最终一致 强一致(ZAB)
性能 最高 中等
自动释放 Lease 机制 过期时间 临时节点
公平锁 Revision 排序 不支持 顺序节点
适用场景 云原生、K8s 高并发缓存 配置中心

选择建议

  • 高并发、性能优先:Redis/Redisson
  • 强一致性、可靠性优先:ZooKeeper/etcd
  • 简单场景、低并发:数据库
  • 云原生环境:etcd

6. Redis 分布式锁怎么实现?

回答:Redis 分布式锁实现方式:

  1. 基础方案SET key value NX EX 30,NX 保证互斥,EX 设置过期防死锁
  2. Redisson 方案:封装了加锁、续期、解锁逻辑,支持看门狗自动续期
  3. RedLock 方案:多个独立 Redis 节点,获取大多数锁才算成功

看门狗机制 :Redisson 的 lock() 方法会启动后台线程,每 10 秒(默认 30 秒的 1/3)检查锁是否还持有,自动续期,防止业务未完成锁过期。

注意事项:解锁时需验证是否是自己的锁(用 Lua 脚本保证原子性);主从切换可能导致锁丢失。


7. RedLock 的原理和争议?

回答:RedLock 是 Redis 作者提出的分布式锁算法:

原理

  1. 获取当前时间
  2. 依次向 N 个独立 Redis 节点请求加锁
  3. 如果在大多数节点(N/2+1)上加锁成功,且总耗时小于锁有效期,则加锁成功
  4. 解锁时向所有节点发送解锁请求

争议(Martin Kleppmann 的批评):

  • 时钟漂移可能导致锁失效
  • GC 停顿可能导致锁过期后仍认为持有锁
  • 网络延迟可能导致多客户端同时持有锁

结论:RedLock 不能保证强一致性,对一致性要求高的场景建议用 ZooKeeper 或 etcd。


8. ZooKeeper 分布式锁的实现原理?

回答:ZooKeeper 分布式锁基于临时顺序节点实现:

  1. 客户端在锁节点下创建临时顺序节点
  2. 获取锁节点下所有子节点,判断自己是否是最小序号
  3. 如果是最小序号,获取锁成功
  4. 如果不是,监听前一个节点的删除事件
  5. 前一个节点删除后,重新判断是否是最小序号

优点 :强一致性(CP)、自动释放(临时节点)、公平锁(顺序节点)
缺点:性能较 Redis 低、需要维护 ZK 集群


9. 缓存和数据库一致性怎么保证?

回答:主要方案:

  1. Cache-Aside(旁路缓存):读时先查缓存,未命中查库再写缓存;写时先更新库再删缓存
  2. 延迟双删:先删缓存、更新库、延迟后再删缓存,减少不一致窗口
  3. 消息队列异步更新:更新库后发消息,消费者异步更新缓存,保证最终一致

为什么删缓存而不是更新缓存:避免并发写导致缓存脏数据;缓存可能需要复杂计算,删除更简单。

并发问题:先更新库再删缓存,可能出现短暂不一致,但概率低、影响小,是最佳实践。


10. 缓存穿透、击穿、雪崩是什么?怎么解决?

回答

问题 定义 解决方案
穿透 查询不存在的数据,缓存和数据库都没有 缓存空值、布隆过滤器、参数校验
击穿 热点 key 过期,大量请求打到数据库 互斥锁、热点数据永不过期、预热
雪崩 大量 key 同时过期,数据库压力骤增 过期时间加随机值、多级缓存、限流降级

11. 分布式 ID 怎么生成?

回答:常见方案:

  1. UUID:简单但无序、太长,不适合做主键
  2. 数据库自增:简单但性能瓶颈、单点问题
  3. 雪花算法:64 位(1 符号位 + 41 时间戳 + 10 机器 ID + 12 序列号),有序、高性能,需解决时钟回拨
  4. 号段模式:从数据库批量获取 ID 段,本地分配,如美团 Leaf
  5. Redis INCR:原子递增,性能好但依赖 Redis

生产推荐:雪花算法或号段模式(Leaf、UidGenerator)。


号段模式详解

原理

  • 从数据库批量获取一段 ID(如 1-1000),存储在本地内存
  • 应用从本地内存中分配 ID,无需每次请求数据库
  • 当本地号段用完后,再次从数据库获取新的号段

数据库表结构

sql 复制代码
CREATE TABLE id_generator (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_tag VARCHAR(128) NOT NULL UNIQUE COMMENT '业务标识',
    max_id BIGINT NOT NULL COMMENT '当前最大ID',
    step INT NOT NULL DEFAULT 1000 COMMENT '号段步长',
    description VARCHAR(256) COMMENT '描述',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 初始化
INSERT INTO id_generator (biz_tag, max_id, step) VALUES ('order', 0, 1000);

核心实现

java 复制代码
@Component
public class SegmentIdGenerator {
    
    @Autowired
    private IdGeneratorMapper idGeneratorMapper;
    
    // 当前号段
    private volatile Segment currentSegment;
    
    // 下一个号段(预加载)
    private volatile Segment nextSegment;
    
    // 加载下一个号段的线程
    private Thread loadingThread;
    
    /**
     * 获取下一个 ID
     */
    public synchronized long nextId(String bizTag) {
        // 1. 检查当前号段是否可用
        if (currentSegment == null || currentSegment.isExhausted()) {
            // 等待下一个号段加载完成
            waitForNextSegment();
            // 切换号段
            currentSegment = nextSegment;
            nextSegment = null;
            // 异步加载下一个号段
            loadNextSegmentAsync(bizTag);
        }
        
        // 2. 从当前号段获取 ID
        return currentSegment.nextId();
    }
    
    /**
     * 从数据库获取号段
     */
    private Segment loadSegment(String bizTag) {
        // 使用乐观锁更新 max_id
        int rows = idGeneratorMapper.updateMaxId(bizTag);
        if (rows == 0) {
            // 更新失败,重试
            return loadSegment(bizTag);
        }
        
        // 查询更新后的 max_id
        IdGenerator generator = idGeneratorMapper.selectByBizTag(bizTag);
        
        long start = generator.getMaxId();
        long end = start + generator.getStep();
        
        // 更新数据库中的 max_id
        idGeneratorMapper.updateMaxIdToEnd(bizTag, end);
        
        return new Segment(start, end);
    }
    
    /**
     * 异步加载下一个号段
     */
    private void loadNextSegmentAsync(String bizTag) {
        if (loadingThread != null && loadingThread.isAlive()) {
            return; // 已在加载中
        }
        
        loadingThread = new Thread(() -> {
            Segment segment = loadSegment(bizTag);
            synchronized (this) {
                nextSegment = segment;
                notifyAll(); // 通知等待的线程
            }
        });
        loadingThread.start();
    }
    
    /**
     * 等待下一个号段加载完成
     */
    private void waitForNextSegment() {
        if (nextSegment == null) {
            // 同步加载第一个号段
            currentSegment = loadSegment("order");
            loadNextSegmentAsync("order");
        } else {
            // 等待异步加载完成
            try {
                while (nextSegment == null) {
                    wait();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

/**
 * 号段类
 */
class Segment {
    private final AtomicLong current;
    private final long end;
    
    public Segment(long start, long end) {
        this.current = new AtomicLong(start);
        this.end = end;
    }
    
    public long nextId() {
        long id = current.getAndIncrement();
        if (id >= end) {
            throw new IllegalStateException("号段已用完");
        }
        return id;
    }
    
    public boolean isExhausted() {
        return current.get() >= end;
    }
    
    public double getIdleRate() {
        return (end - current.get()) / (double)(end - current.get() + current.get());
    }
}

Mapper 接口

java 复制代码
@Mapper
public interface IdGeneratorMapper {
    
    /**
     * 乐观锁更新 max_id
     */
    @Update("UPDATE id_generator SET max_id = max_id + step WHERE biz_tag = #{bizTag}")
    int updateMaxId(@Param("bizTag") String bizTag);
    
    /**
     * 更新 max_id 到指定值
     */
    @Update("UPDATE id_generator SET max_id = #{maxId} WHERE biz_tag = #{bizTag}")
    int updateMaxIdToEnd(@Param("bizTag") String bizTag, @Param("maxId") long maxId);
    
    /**
     * 查询
     */
    @Select("SELECT * FROM id_generator WHERE biz_tag = #{bizTag}")
    IdGenerator selectByBizTag(@Param("bizTag") String bizTag);
}

美团 Leaf 的实现特点

  1. 双 Buffer 机制

    • 当前号段使用到 10% 时,异步加载下一个号段
    • 两个号段交替使用,保证 ID 分配的连续性
  2. 监控告警

    • 监控号段使用率
    • 号段即将用完时告警
  3. 高可用

    • 支持多数据库实例
    • 数据库故障时,使用本地缓存

号段模式的优点

  • 性能好:批量获取,减少数据库访问
  • 趋势递增:ID 有序,适合数据库主键
  • 无单点故障:支持多数据库实例
  • 无时钟依赖:不依赖系统时钟

号段模式的缺点

  • 依赖数据库:需要数据库支持
  • 号段浪费:服务重启时,未使用的号段会丢失
  • ID 不连续:不同业务或实例的 ID 不连续

与雪花算法对比

特性 号段模式 雪花算法
性能 高(批量获取) 极高(本地生成)
有序性 趋势递增 趋势递增
依赖 数据库 系统时钟
ID 长度 可配置 固定 64 位
适用场景 数据库主键 分布式系统

12. 雪花算法是怎么实现的?

回答:雪花算法是 Twitter 开源的分布式 ID 生成算法,生成 64 位 long 型 ID。

64 位结构

部分 位数 说明
符号位 1 固定为 0,保证 ID 为正数
时间戳 41 毫秒级,可用约 69 年
机器 ID 10 5 位数据中心 + 5 位机器,支持 1024 个节点
序列号 12 同一毫秒内的序列,支持 4096 个 ID

核心流程

  1. 获取当前时间戳,检测时钟回拨
  2. 同一毫秒内序列号递增,序列号用尽则等待下一毫秒
  3. 新毫秒序列号重置为 0
  4. 组装 ID:(时间戳差值 << 22) | (机器ID << 12) | 序列号

优点 :高性能(本地生成)、趋势递增(利于索引)、不依赖外部存储
缺点:依赖系统时钟,时钟回拨会导致 ID 重复或生成失败


13. 雪花算法的时钟回拨问题怎么解决?

回答:时钟回拨是指系统时间因 NTP 同步等原因往回调,导致当前时间戳小于上次生成 ID 的时间戳。

产生的问题

  • ID 重复:回拨后时间戳相同,可能生成重复 ID
  • ID 不递增:新 ID 比旧 ID 小,破坏趋势递增特性
  • 服务不可用:如果直接抛异常,会导致 ID 生成失败

解决方案

方案 实现 优点 缺点
抛异常 检测到回拨直接报错 简单 影响可用性
等待 回拨时间短则自旋等待 简单、不影响 ID 回拨大时阻塞久
备用机器 ID 预留多个机器 ID,回拨时切换 不阻塞 浪费机器 ID
扩展位 用 2-3 bit 记录回拨次数 不阻塞、不浪费 实现复杂
外部时钟 依赖 ZK/DB 获取时间戳(美团 Leaf) 可靠 增加依赖

14. 消息队列如何保证消息不丢失?

回答:从三个环节保证:

  1. 生产端:开启确认机制(RocketMQ 的 SendResult、Kafka 的 acks=all)
  2. Broker 端:消息持久化、多副本同步(Kafka ISR、RocketMQ 主从同步)
  3. 消费端:手动确认(ACK),消费成功后再提交偏移量

RocketMQ:同步发送 + 同步刷盘 + 主从同步 + 手动 ACK

Kafka:acks=all + min.insync.replicas + 手动提交偏移量


15. 消息队列如何保证消息不重复消费?

回答:消息队列只能保证 At Least Once,去重需要业务端实现幂等:

  1. 唯一 ID + 去重表:消费前查询是否已处理
  2. 数据库唯一索引:利用唯一约束防重
  3. Redis Set:消费前 SETNX 判断
  4. 状态机:业务状态流转,已处理的状态不再处理
  5. 乐观锁:版本号控制,重复消费不会更新

16. 消息队列如何保证消息顺序性?

回答

  1. 全局顺序:单分区/单队列,性能差
  2. 分区顺序 :相同业务 key 的消息发到同一分区,分区内有序
    • RocketMQ:MessageQueueSelector 指定队列
    • Kafka:指定 partition 或相同 key

注意:消费端也要保证顺序,单线程消费或分区内串行处理。


17. 微服务之间如何保证事务?

回答

  1. Seata 分布式事务

    • AT 模式:无侵入,自动补偿
    • TCC 模式:高并发,手动实现 Try/Confirm/Cancel
    • Saga 模式:长事务,正向操作 + 补偿操作
  2. 消息队列 + 本地消息表

    • 业务操作和消息写入同一本地事务
    • 定时任务扫描消息表发送消息
    • 消费端处理后回调确认
  3. 最大努力通知

    • 多次重试通知,不保证一定成功
    • 适合对一致性要求不高的场景

选择:强一致用 Seata,最终一致用消息队列。


18. 什么是脑裂问题?怎么解决?

回答:脑裂是指分布式系统因网络分区,分裂成多个独立部分,每部分都认为自己是主节点,导致数据不一致。

解决方案

  1. Quorum 机制:只有获得大多数节点支持才能成为主节点
  2. Fencing Token:每次选主生成递增的 token,旧主的操作会被拒绝
  3. Lease 机制:主节点持有租约,过期前其他节点不能成为主
  4. STONITH:Shoot The Other Node In The Head,强制关闭疑似故障节点

Redis Sentinel/Cluster、ZooKeeper、etcd 都通过 Quorum 机制避免脑裂。


19. 服务注册发现的原理?Nacos 和 Eureka 的区别?

回答

原理

  1. 服务启动时向注册中心注册(IP、端口、服务名)
  2. 消费者从注册中心获取服务列表
  3. 注册中心通过心跳检测服务健康状态
  4. 服务下线时从注册中心注销

Nacos vs Eureka

特性 Nacos Eureka
一致性 CP + AP 可切换 AP
健康检查 主动探测 + 心跳 仅心跳
配置中心 支持 不支持
雪崩保护 支持 支持
维护状态 活跃 停止维护

推荐使用 Nacos,功能更全面。


20. 熔断和限流的区别?Sentinel 怎么实现?

回答

区别

  • 限流:控制请求速率,防止系统过载
  • 熔断:服务异常时快速失败,防止级联故障

Sentinel 实现

  1. 限流:支持 QPS 限流、并发线程数限流
  2. 熔断:慢调用比例、异常比例、异常数三种策略
  3. 流量整形:直接拒绝、Warm Up、排队等待

熔断状态:关闭 → 打开 → 半开 → 关闭/打开


21. 分库分表怎么做?有什么问题?

回答

分片策略

  • 水平分表:按行拆分,如按用户 ID 取模
  • 垂直分表:按列拆分,冷热数据分离
  • 水平分库:数据分散到多个数据库

常见问题

  1. 跨库 Join:应用层组装或冗余数据
  2. 分布式事务:Seata 或最终一致性
  3. 全局 ID:雪花算法、号段模式
  4. 数据迁移:双写、增量同步
  5. 扩容:一致性哈希减少数据迁移

中间件:ShardingSphere、MyCat


22. 如何设计一个高可用系统?

回答

  1. 冗余设计:多副本、多机房、异地多活
  2. 负载均衡:Nginx、Gateway 分发流量
  3. 服务治理:注册发现、熔断限流、降级
  4. 数据层:主从复制、读写分离、分库分表
  5. 缓存层:多级缓存、缓存预热、热点数据处理
  6. 消息队列:异步解耦、削峰填谷
  7. 监控告警:链路追踪、日志分析、实时告警
  8. 故障演练:混沌工程、定期演练

核心指标:可用性(99.99%)、响应时间、吞吐量、故障恢复时间(RTO)。


23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?

回答:这是一个典型的分布式事务场景,根据不同的需求有两种场景:

方案列表

场景一:AB 事务提交后 C 才开始执行

  • 方案一:RocketMQ 事务消息(推荐)
  • 方案二:本地消息表
  • 方案三:消息队列 + 延迟检查
  • 方案四:2PC(两阶段提交)

场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交

  • 方案一:TransactionSynchronization + 编程式事务(推荐)
  • 方案二:CountDownLatch + 编程式事务
  • 方案三:CompletableFuture + 编程式事务

场景一:AB 事务提交后 C 才开始执行

需求:C 操作必须在 AB 事务提交完成后才开始执行(顺序执行)。

方案:RocketMQ 事务消息(推荐)

原理:使用 RocketMQ 的事务消息机制,通过半消息(Half Message)和本地事务监听器(Local Transaction Listener)保证消息发送和本地事务的一致性。

执行流程

  1. 生产者发送半消息到 Broker(消息对消费者不可见)
  2. Broker 收到半消息后,立即回调 executeLocalTransaction 方法
  3. executeLocalTransaction 中执行 A 和 B 操作(在 @Transactional 事务中)
  4. 如果 A 或 B 失败,返回 ROLLBACK,消息被删除,C 不会执行
  5. 如果 A 和 B 都成功,返回 COMMIT,消息变为可见
  6. 消费者消费消息,执行 C 操作(此时 AB 事务已提交)

特点

  • ✅ 保证 C 操作执行时,AB 事务已经提交完成
  • ❌ A 和 C 不能并行执行,C 必须等待 AB 事务提交后才开始执行

核心代码

java 复制代码
// 1. 生产者:发送事务消息
@Service
public class OrderService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    public void executeAAndB(Order order) {
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "order-topic",
            MessageBuilder.withPayload(order)
                .setHeader("orderId", order.getId())
                .build(),
            order
        );
    }
}

// 2. 事务监听器:执行本地事务(A 和 B 操作)
@Component
@RocketMQTransactionListener(txProducerGroup = "order-producer-group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountMapper accountMapper;
    
    /**
     * 执行本地事务(A 和 B 操作)
     * 
     * 作用:Broker 收到半消息后立即回调此方法,执行本地事务(A 和 B 操作)
     * 执行时机:同步执行,在 sendMessageInTransaction 方法中阻塞等待
     * 事务边界:@Transactional 保证 A 和 B 在同一事务中
     * 返回值:
     *   - COMMIT:事务提交成功,消息变为可见,消费者可以消费(执行 C 操作)
     *   - ROLLBACK:事务回滚,消息被删除,消费者看不到,C 操作不会执行
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            Order order = (Order) arg;
            // A 操作:插入订单
            orderMapper.insertOrder(order);
            // B 操作:扣减账户余额
            accountMapper.deduct(order.getUserId(), order.getAmount());
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
    
    /**
     * 检查本地事务状态(解决事务状态不确定问题)
     * 
     * 作用:当 executeLocalTransaction 返回 UNKNOWN 或网络异常时,Broker 会回查此方法
     * 执行时机:消息发送后 1 分钟首次回查,之后每 1 分钟回查一次,最多 15 次
     * 触发条件:
     *   - executeLocalTransaction 返回 UNKNOWN
     *   - 网络超时,Broker 未收到 executeLocalTransaction 的返回值
     *   - 应用崩溃,事务状态未知
     * 实现方式:通过查询业务数据(订单是否存在)判断事务是否已提交
     * 返回值:
     *   - COMMIT:事务已提交,消息变为可见,消费者可以消费
     *   - ROLLBACK:事务已回滚,消息被删除,消费者看不到
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String orderId = (String) msg.getHeaders().get("orderId");
        Order order = orderMapper.selectById(orderId);
        if (order != null && order.getStatus() == 1) {
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

// 3. 消费者:执行 C 操作
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer-group")
public class OrderConsumer implements RocketMQListener<Order> {
    
    @Autowired
    private InventoryService inventoryService;
    
    @Override
    public void onMessage(Order order) {
        // C 操作:扣减库存
        inventoryService.deductInventory(order.getProductId(), order.getQuantity());
    }
}

优点

  • 保证消息发送和本地事务的一致性
  • 自动处理事务状态不确定问题(checkLocalTransaction)
  • 对业务代码侵入性小

缺点

  • 依赖 RocketMQ 的事务消息功能
  • 需要实现事务状态检查逻辑
  • A 和 C 不能并行执行,C 必须等待 AB 事务提交后才开始执行

场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交

需求:A 和 C 可以并行执行(不阻塞),但 C 的事务提交必须等待 AB 事务提交完成。

方案:TransactionSynchronization + 编程式事务(推荐)

原理 :使用 Spring 的 TransactionSynchronization 监听 AB 事务的提交,C 操作使用编程式事务手动控制提交时机。

执行流程

  1. 执行 A 操作(在 AB 事务中)
  2. 同时启动 C 操作(在另一个事务中,使用编程式事务,不自动提交)
  3. 执行 B 操作(在 AB 事务中)
  4. 注册 TransactionSynchronization 监听 AB 事务提交
  5. AB 事务提交(@Transactional 自动提交)
  6. afterCommit 回调触发,通知 C 操作
  7. C 操作提交事务(此时 AB 已提交)

核心代码

java 复制代码
// 1. 订单服务
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Transactional(rollbackFor = Exception.class)
    public void executeAAndBWithParallelC(Order order) {
        CountDownLatch abCommitLatch = new CountDownLatch(1);
        AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
        AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
        
        // 执行 A 操作
        orderMapper.insertOrder(order);
        
        // 启动 C 操作(异步执行,与 A 并行)
        CompletableFuture.runAsync(() -> {
            try {
                TransactionStatus status = inventoryService.prepareCWithoutCommit(order);
                cTransactionStatusRef.set(status);
                abCommitLatch.await();
                
                if (abCommitSuccess.get()) {
                    inventoryService.commitC(order, cTransactionStatusRef.get());
                } else {
                    inventoryService.rollbackC(order, cTransactionStatusRef.get());
                }
            } catch (Exception e) {
                if (cTransactionStatusRef.get() != null) {
                    inventoryService.rollbackC(order, cTransactionStatusRef.get());
                }
            }
        });
        
        // 执行 B 操作
        order.setStatus(1);
        orderMapper.updateOrderStatus(order);
        
        // 注册事务同步器,监听 AB 事务提交
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                abCommitSuccess.set(true);
                abCommitLatch.countDown();
            }
            
            @Override
            public void afterCompletion(int status) {
                if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
                    abCommitSuccess.set(false);
                    abCommitLatch.countDown();
                }
            }
        });
    }
}

// 2. 库存服务
@Service
public class InventoryService {
    @Autowired
    private InventoryMapper inventoryMapper;
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    public TransactionStatus prepareCWithoutCommit(Order order) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status = transactionManager.getTransaction(def);
        
        try {
            inventoryMapper.deductInventory(order.getProductId(), order.getQuantity());
            return status;
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
    
    public void commitC(Order order, TransactionStatus status) {
        if (status != null && !status.isCompleted()) {
            transactionManager.commit(status);
        }
    }
    
    public void rollbackC(Order order, TransactionStatus status) {
        if (status != null && !status.isCompleted()) {
            transactionManager.rollback(status);
        }
    }
}

优点

  • ✅ A 和 C 可以并行执行,提高性能
  • ✅ C 的事务提交等待 AB 事务提交完成,保证一致性
  • ✅ 使用 Spring 原生机制,不依赖外部组件

缺点

  • 需要手动管理事务状态
  • 实现相对复杂
  • 需要处理多线程同步问题

方案二:CountDownLatch + 编程式事务

原理 :使用 CountDownLatch 作为信号量,让 C 操作等待 AB 事务提交完成,C 操作使用编程式事务手动控制提交时机。

核心代码

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Transactional(rollbackFor = Exception.class)
    public void executeAAndBWithParallelC(Order order) {
        CountDownLatch abCommitLatch = new CountDownLatch(1);
        AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
        AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
        
        // 执行 A 操作
        orderMapper.insertOrder(order);
        
        // 启动 C 操作(异步执行,与 A 并行)
        CompletableFuture.runAsync(() -> {
            try {
                // C 操作执行(使用编程式事务,不自动提交)
                DefaultTransactionDefinition def = new DefaultTransactionDefinition();
                def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
                TransactionStatus status = transactionManager.getTransaction(def);
                cTransactionStatusRef.set(status);
                
                inventoryService.deductInventory(order.getProductId(), order.getQuantity());
                
                // 等待 AB 事务提交完成
                abCommitLatch.await();
                
                if (abCommitSuccess.get()) {
                    // AB 事务提交成功,C 操作提交事务
                    transactionManager.commit(cTransactionStatusRef.get());
                } else {
                    // AB 事务回滚,C 操作也回滚
                    transactionManager.rollback(cTransactionStatusRef.get());
                }
            } catch (Exception e) {
                if (cTransactionStatusRef.get() != null) {
                    transactionManager.rollback(cTransactionStatusRef.get());
                }
            }
        });
        
        // 执行 B 操作
        order.setStatus(1);
        orderMapper.updateOrderStatus(order);
        
        // 方法返回时,@Transactional 自动提交 AB 事务
        // 在 finally 块中释放 CountDownLatch(实际应该在事务提交后释放)
        // 注意:这里需要确保在事务提交后才释放,可以使用 TransactionSynchronization
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                abCommitSuccess.set(true);
                abCommitLatch.countDown();
            }
            
            @Override
            public void afterCompletion(int status) {
                if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
                    abCommitSuccess.set(false);
                    abCommitLatch.countDown();
                }
            }
        });
    }
}

优点

  • 实现直观,易于理解
  • 使用 CountDownLatch 作为信号量,逻辑清晰

缺点

  • 仍然需要 TransactionSynchronization 来确保在事务提交后释放信号量
  • 需要手动管理事务状态

方案三:CompletableFuture + 编程式事务

原理 :使用 CompletableFuture 的链式调用和组合能力,灵活处理异步执行和事务提交时机,通过 thenComposethenApply 等方法组合多个异步操作。

核心代码

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Transactional(rollbackFor = Exception.class)
    public void executeAAndBWithParallelC(Order order) {
        AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
        AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
        CountDownLatch abCommitLatch = new CountDownLatch(1);
        
        // 执行 A 操作
        orderMapper.insertOrder(order);
        
        // 启动 C 操作(异步执行,与 A 并行)
        CompletableFuture<TransactionStatus> cFuture = CompletableFuture.supplyAsync(() -> {
            try {
                // C 操作执行(使用编程式事务,不自动提交)
                DefaultTransactionDefinition def = new DefaultTransactionDefinition();
                def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
                TransactionStatus status = transactionManager.getTransaction(def);
                
                inventoryService.deductInventory(order.getProductId(), order.getQuantity());
                return status;
            } catch (Exception e) {
                throw new RuntimeException("C 操作执行失败", e);
            }
        });
        
        // 执行 B 操作
        order.setStatus(1);
        orderMapper.updateOrderStatus(order);
        
        // 注册事务同步器,监听 AB 事务提交
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                abCommitSuccess.set(true);
                abCommitLatch.countDown();
            }
            
            @Override
            public void afterCompletion(int status) {
                if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
                    abCommitSuccess.set(false);
                    abCommitLatch.countDown();
                }
            }
        });
        
        // 使用 CompletableFuture 链式调用:等待 C 操作完成 → 等待 AB 事务提交 → 提交/回滚 C 操作
        cFuture.thenCompose(cStatus -> {
            cTransactionStatusRef.set(cStatus);
            return CompletableFuture.runAsync(() -> {
                try {
                    abCommitLatch.await();
                    if (abCommitSuccess.get()) {
                        transactionManager.commit(cStatus);
                    } else {
                        transactionManager.rollback(cStatus);
                    }
                } catch (Exception e) {
                    transactionManager.rollback(cStatus);
                }
            });
        }).exceptionally(e -> {
            // 处理异常
            if (cTransactionStatusRef.get() != null) {
                transactionManager.rollback(cTransactionStatusRef.get());
            }
            return null;
        });
    }
}

优点

  • 使用 CompletableFuture 的链式调用,代码更灵活
  • 支持复杂的异步组合场景
  • 可以方便地组合多个异步操作

缺点

  • 实现相对复杂
  • 需要理解 CompletableFuture 的执行机制
  • 错误处理需要额外注意

方案二:本地消息表(适用于场景一)

原理:将消息写入本地数据库,与业务操作在同一事务中,通过定时任务异步发送消息。

执行流程

  1. A 和 B 操作与消息写入在同一本地事务中
  2. 事务提交后,消息状态为"待发送"
  3. 定时任务扫描消息表,发送消息到消息队列
  4. 消费者消费消息,执行 C 操作

核心代码

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Autowired
    private LocalMessageMapper localMessageMapper;
    
    /**
     * 执行 A 和 B 操作,并写入消息表
     */
    @Transactional
    public void executeAAndB() {
        // A 操作:创建订单
        Order order = new Order();
        orderMapper.insert(order);
        
        // B 操作:扣减账户余额
        accountMapper.deduct(order.getUserId(), order.getAmount());
        
        // 写入消息表(在同一事务中)
        LocalMessage message = new LocalMessage();
        message.setBusinessId(order.getId());
        message.setContent(JSON.toJSONString(order));
        message.setStatus("PENDING");
        localMessageMapper.insert(message);
        
        // 事务提交后,消息状态为"待发送"
    }
}

// 定时任务:发送消息
@Component
public class MessageSender {
    
    @Scheduled(fixedDelay = 5000)
    public void sendPendingMessages() {
        List<LocalMessage> messages = localMessageMapper.selectByStatus("PENDING");
        for (LocalMessage msg : messages) {
            try {
                rocketMQTemplate.send("order-topic", msg.getContent());
                msg.setStatus("SENT");
                localMessageMapper.update(msg);
            } catch (Exception e) {
                // 发送失败,重试
                msg.setRetryCount(msg.getRetryCount() + 1);
                localMessageMapper.update(msg);
            }
        }
    }
}

优点

  • 实现简单,不依赖特定消息队列
  • 保证消息一定会发送(定时任务重试)

缺点

  • 消息发送有延迟(定时任务扫描间隔)
  • 需要维护消息表
  • 可能出现重复发送(需要消费端做幂等)

方案三:消息队列 + 延迟检查

原理:先发送消息,消费者收到消息后延迟检查业务数据,确保 AB 事务已提交。

执行流程

  1. A 操作执行时发送消息(消息对消费者可见)
  2. 消费者收到消息后,延迟一段时间(如 1 秒)
  3. 延迟后检查业务数据(订单是否存在)
  4. 如果订单存在,执行 C 操作;否则丢弃消息

核心代码

java 复制代码
// 生产者
@Service
public class OrderService {
    
    @Transactional
    public void executeAAndB() {
        // A 操作:创建订单
        Order order = new Order();
        orderMapper.insert(order);
        
        // 发送消息(事务未提交)
        rocketMQTemplate.send("order-topic", JSON.toJSONString(order));
        
        // B 操作:扣减账户余额
        accountMapper.deduct(order.getUserId(), order.getAmount());
        
        // 事务提交
    }
}

// 消费者
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener<String> {
    
    @Override
    public void onMessage(String message) {
        Order order = JSON.parseObject(message, Order.class);
        
        // 延迟检查:等待 AB 事务提交
        Thread.sleep(1000);
        
        // 检查订单是否存在
        Order dbOrder = orderMapper.selectById(order.getId());
        if (dbOrder != null && dbOrder.getStatus() == 1) {
            // 订单存在,说明 AB 事务已提交,执行 C 操作
            inventoryService.deduct(order.getProductId(), order.getQuantity());
        } else {
            // 订单不存在,说明 AB 事务已回滚,丢弃消息
            log.warn("订单不存在,丢弃消息: {}", order.getId());
        }
    }
}

优点

  • 实现简单
  • 不依赖特定消息队列功能

缺点

  • 延迟时间不确定(可能过长或过短)
  • 可能出现消息丢失(事务回滚但消息已发送)
  • 需要消费端做幂等处理

方案四:2PC(两阶段提交)

适用场景场景一(AB 事务提交后 C 才开始执行)

原理:使用分布式事务协调器,保证所有参与者的操作要么全部提交,要么全部回滚。

执行流程

  1. 协调者向所有参与者(A、B、C)发送 prepare 请求
  2. 参与者执行操作但不提交,返回 Yes/No
  3. 如果所有参与者都返回 Yes,协调者发送 commit
  4. 所有参与者提交事务

为什么适用于场景一,不适用于场景二

特性 场景一需求 场景二需求 2PC 支持情况
执行顺序 C 等待 AB 提交后才开始执行 A 和 C 可以并行执行 ✅ 支持(顺序执行)
并行性 不需要并行 需要 A 和 C 并行 ❌ 不支持(同步阻塞)
事务提交 C 在 AB 提交后执行 C 在 AB 提交后提交 ✅ 支持(统一提交)
性能 顺序执行,性能可接受 需要并行,性能要求高 ❌ 性能差(同步阻塞)

2PC 的特点

  • 顺序执行:所有操作必须按顺序执行,等待协调者指令
  • 统一提交:所有参与者要么全部提交,要么全部回滚
  • 不支持并行:所有操作都是同步阻塞的,无法并行执行
  • 性能差:同步阻塞导致性能较差

2PC 执行流程示例

复制代码
场景一(2PC 适用):
T1: 协调者发送 prepare 给 A、B、C
T2: A 执行操作,返回 Yes(不提交)
T3: B 执行操作,返回 Yes(不提交)
T4: C 执行操作,返回 Yes(不提交)
T5: 协调者收到所有 Yes,发送 commit
T6: A 提交事务
T7: B 提交事务
T8: C 提交事务
✅ 所有操作顺序执行,统一提交

场景二(2PC 不适用):
❌ 2PC 无法让 A 和 C 并行执行
❌ 2PC 必须等待所有参与者都 prepare 完成后才能 commit
❌ 无法实现"AC 并行执行,但 C 提交等待 AB 提交"的需求

缺点

  • 性能较差(同步阻塞,所有操作必须顺序执行)
  • 单点故障风险(协调者宕机导致整个事务失败)
  • 实现复杂(需要协调者、参与者、网络通信等)
  • 不支持并行执行(无法满足场景二的需求)

适用场景

  • 场景一:C 操作可以等待 AB 事务提交后再开始执行
  • 场景二:A 和 C 需要并行执行(2PC 不支持)

方案对比

场景一:AB 事务提交后 C 才开始执行
方案 优点 缺点 适用场景
RocketMQ 事务消息 保证一致性,自动处理状态不确定 依赖 RocketMQ,A 和 C 不能并行 推荐使用
本地消息表 实现简单,不依赖特定 MQ 消息发送有延迟 对延迟不敏感的场景
延迟检查 实现简单 延迟不确定,可能丢消息 不推荐
2PC 强一致性 性能差,实现复杂 不推荐

推荐 :优先使用 RocketMQ 事务消息 ,如果消息队列不支持事务消息,使用 本地消息表

场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交
方案 优点 缺点 适用场景
TransactionSynchronization + 编程式事务 A 和 C 并行执行,C 提交等待 AB 提交,使用 Spring 原生机制 需要手动管理事务状态,实现相对复杂 推荐使用(需要并行执行时)
CountDownLatch + 编程式事务 直观,易于理解 需要手动管理信号量 适合简单场景
CompletableFuture + 编程式事务 灵活,支持链式调用 实现相对复杂 适合复杂异步场景

推荐 :优先使用 TransactionSynchronization + 编程式事务,它是 Spring 提供的标准机制,适合需要 A 和 C 并行执行的场景。

场景选择
  • 场景一:如果 C 操作可以等待 AB 事务提交后再开始执行,使用 RocketMQ 事务消息
  • 场景二:如果 A 和 C 需要并行执行以提高性能,但 C 的事务提交必须等待 AB 事务提交完成,使用 TransactionSynchronization + 编程式事务
相关推荐
程序员小远1 小时前
软件测试之单元测试详解
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
做怪小疯子5 小时前
LeetCode 热题 100——二叉树——二叉树的层序遍历&将有序数组转换为二叉搜索树
算法·leetcode·职场和发展
面试鸭5 小时前
科大讯飞,你好大方。。。
java·计算机·职场和发展·求职招聘
i***66506 小时前
分布式推理框架 xDit
分布式
Heo6 小时前
关于Gulp,你学这些就够了
前端·javascript·面试
哈哈哈笑什么6 小时前
分布式事务实战:订单服务 + 库存服务(基于本地消息表组件)
分布式·后端·rabbitmq
哈哈哈笑什么6 小时前
完整分布式事务解决方案(本地消息表 + RabbitMQ)
分布式·后端·rabbitmq