Skip to content

十四、分布式事务(8 题)

14.1 ★★★★CAP 理论和 BASE 理论分别是什么?分布式事务产生的根本原因是什么?

📖 点击查看答案

CAP 理论

  • C(Consistency 一致性):所有节点数据一致,读操作总能读到最新写
  • A(Availability 可用性):每个请求都能收到非错误响应(不保证最新)
  • P(Partition Tolerance 分区容错):网络分区时系统仍能工作
  • 核心结论:分布式系统只能满足 CAP 中的两个,由于 P 必选,故在 CP 和 AP 间权衡
    • CP:ZooKeeper、etcd、HBase(强一致,分区时不可用)
    • AP:Eureka、Cassandra、Redis 集群(高可用,允许最终一致)

BASE 理论(CAP 的 AP 延伸):

  • B(Basically Available 基本可用):故障时允许损失部分可用性(响应时间增加、降级)
  • S(Soft state 软状态):允许中间状态存在,不要求时时一致
  • E(Eventually consistent 最终一致性):系统保证最终达到一致状态

分布式事务产生的根本原因

  1. 数据分散存储:单机 ACID 事务无法覆盖跨库/跨服务的数据操作
  2. 微服务拆分:一个业务操作涉及多个服务,每个服务有独立数据库
  3. 网络不可靠:节点间通信可能失败,导致部分成功部分失败
  4. 缺乏全局协调:本地事务无法感知其他参与者状态,无法统一提交/回滚
java
// 典型场景:电商下单
// 订单服务(order_db):插入订单
// 库存服务(stock_db):扣减库存
// 账户服务(account_db):扣减余额
// 三者跨库跨服务,需要分布式事务保证一致性

@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);           // 本地事务
        stockFeignClient.deduct(order);      // 远程调用 - 可能失败
        accountFeignClient.debit(order);     // 远程调用 - 可能失败
        // 若 accountFeignClient 失败,订单已生成、库存已扣减 → 数据不一致
    }
}

最终一致性方案演进

  • 强一致(2PC/XA)→ 性能差
  • 柔性事务(TCC/Saga)→ 业务侵入
  • 最终一致(本地消息表/事务消息)→ 异步、解耦

14.2 ★★★★2PC(两阶段提交)协议的流程是怎样的?它有哪些缺陷?3PC 相比 2PC 有哪些改进?

📖 点击查看答案

2PC(两阶段提交)流程

阶段一:准备阶段(Prepare)

  1. 协调者发送 prepare 请求给所有参与者
  2. 参与者执行事务操作(写 undo/redo log),不提交
  3. 参与者反馈 yes(可提交)或 no(回滚)
  4. 协调者等待所有参与者响应(超时未响应视为 no)

阶段二:提交/回滚阶段(Commit/Rollback)

  • 所有参与者都 yes:协调者发送 commit,参与者提交并释放资源
  • 任一参与者 no 或超时:协调者发送 rollback,参与者回滚
java
// 伪代码示意
class Coordinator {
    void twoPhaseCommit() {
        // Phase 1
        boolean allYes = true;
        for (Participant p : participants) {
            if (!p.prepare()) { allYes = false; break; }
        }
        // Phase 2
        for (Participant p : participants) {
            if (allYes) p.commit();
            else p.rollback();
        }
    }
}

2PC 缺陷

  1. 同步阻塞:参与者锁定资源等待协调者,期间阻塞其他事务
  2. 单点故障:协调者宕机,参与者一直阻塞
  3. 数据不一致:Commit 阶段部分参与者收到 commit,部分未收到(网络分区)
  4. 性能差:两轮 RPC + 资源锁定,TPS 低
  5. 容错性差:任一参与者故障导致整个事务阻塞

3PC(三阶段提交)改进

阶段3PC 说明
CanCommit询问是否可提交,不锁定资源(轻量级 check)
PreCommit执行事务,写日志,准备提交(相当于 2PC 的 prepare)
DoCommit真正提交

3PC 改进点

  1. 引入超时机制:参与者超时后自动提交/回滚,减少阻塞
    • 参与者在 PreCommit 后未收到 DoCommit,超时自动 commit
  2. CanCommit 预检查:提前发现不可提交的情况,减少资源锁定时间
  3. 降低阻塞范围:失败时参与者能自行决策

3PC 仍未解决的问题

  • 数据不一致风险(DoCommit 网络分区,部分自动 commit 部分回滚)
  • 网络分区下仍不安全
  • 实际应用较少(复杂度高,性能提升有限)

14.3 ★★★★Seata 的 AT 模式原理是什么?它的 undo_log 机制是如何工作的?AT 模式和 TCC 模式的对比?

📖 点击查看答案

Seata AT 模式核心概念

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

AT 模式流程

一阶段(业务执行)

  1. TM 向 TC 开启全局事务,获取 XID
  2. SQL 解析:拦截 SQL,生成 before image(修改前数据)
  3. 执行业务 SQL
  4. 生成 after image(修改后数据)
  5. 将 before/after image 存入 undo_log 表(与业务 SQL 同一本地事务提交)
  6. 向 TC 注册分支事务,加全局锁
  7. 本地事务提交,立即释放本地锁(这是 AT 性能高的关键)

二阶段提交

  • TC 通知各 RM 提交,RM 异步删除 undo_log(性能高)

二阶段回滚

  1. RM 根据 undo_log 的 before image 生成反向 SQL
  2. 校验 after image 与当前数据是否一致(防止脏写)
  3. 执行反向 SQL 恢复数据
  4. 删除 undo_log
sql
-- undo_log 表结构
CREATE TABLE undo_log (
    branch_id     BIGINT,
    xid           VARCHAR(128),
    context       VARCHAR(128),
    rollback_info LONGBLOB,  -- 存储 before/after image
    log_status    INT,
    log_created   DATETIME,
    log_modified  DATETIME,
    UNIQUE KEY ux_undo_log (xid, branch_id)
);
java
// Seata AT 使用
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count) {
    storageFeignClient.deduct(commodityCode, count);  // 分支事务1
    orderFeignClient.create(userId, commodityCode, count);  // 分支事务2
    accountFeignClient.debit(userId, count * 100);    // 分支事务3
    // 任一失败,全局回滚,根据 undo_log 反向恢复
}

AT vs TCC 对比

维度AT 模式TCC 模式
侵入性无(自动拦截 SQL)高(需手写 Try/Confirm/Cancel)
一致性最终一致(一阶段提交,存在脏读)最终一致(业务隔离)
性能高(一阶段即提交)高(不依赖数据库事务)
隔离性弱(需全局锁辅助)业务层隔离
回滚方式自动反向 SQL手写 Cancel 逻辑
适用场景关系型数据库、简单 CRUD非数据库资源(如扣库存)、需强隔离
限制需要数据库支持事务、有主键业务复杂度高

AT 模式隔离性问题

  • 写隔离:通过全局锁保证(一阶段提交前获取全局锁)
  • 读隔离:默认读未提交,需 @GlobalLock + SELECT FOR UPDATE 实现读已提交

14.4 ★★★★TCC(Try-Confirm-Cancel)模式的原理是什么?如何保证空回滚、幂等、悬挂三个问题?

📖 点击查看答案

TCC 原理

  • Try:资源预留(如冻结余额、预占库存),不真正扣减
  • Confirm:确认执行,真正扣减预留资源
  • Cancel:取消执行,释放预留资源
java
// 账户服务 TCC 实现
public interface AccountTccAction {
    @TwoPhaseBusinessAction(name = "debit", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryDebit(BusinessActionContext ctx, String userId, BigDecimal amount);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);
}

@Service
public class AccountTccActionImpl implements AccountTccAction {
    // Try:冻结金额
    public boolean tryDebit(BusinessActionContext ctx, String userId, BigDecimal amount) {
        // 检查幂等性(事务记录表)
        if (exists(ctx.getXid(), ctx.getBranchId())) return true;
        // 检查并冻结
        accountMapper.freeze(userId, amount);  // available -= amount, frozen += amount
        // 记录事务日志
        insertTccLog(ctx.getXid(), ctx.getBranchId(), "TRY");
        return true;
    }
    // Confirm:扣减冻结金额
    public boolean confirm(BusinessActionContext ctx) {
        if (exists(ctx.getXid(), ctx.getBranchId())) return true; // 幂等
        accountMapper.deductFrozen(ctx.getActionContext("userId"), ctx.getActionContext("amount"));
        updateTccLog(ctx.getXid(), ctx.getBranchId(), "CONFIRM");
        return true;
    }
    // Cancel:解冻
    public boolean cancel(BusinessActionContext ctx) {
        // 空回滚检查:若 Try 未执行,直接插入标记
        if (!exists(ctx.getXid(), ctx.getBranchId())) {
            insertTccLog(ctx.getXid(), ctx.getBranchId(), "CANCEL_EMPTY");
            return true;
        }
        // 悬挂检查:若已 Cancel,不再执行
        if (isCancelled(ctx.getXid(), ctx.getBranchId())) return true;
        accountMapper.unfreeze(ctx.getActionContext("userId"), ctx.getActionContext("amount"));
        updateTccLog(ctx.getXid(), ctx.getBranchId(), "CANCEL");
        return true;
    }
}

三大问题及解决方案

1. 空回滚

  • 问题:Try 未执行(如超时/网络问题),但 Cancel 被调用
  • 解决:Cancel 前检查 Try 是否执行(查事务记录表),未执行则插入"空回滚"标记直接返回

2. 幂等性

  • 问题:Confirm/Cancel 可能被重试调用
  • 解决:事务记录表记录状态(TRY/CONFIRM/CANCEL),执行前检查状态,已执行则直接返回

3. 悬挂

  • 问题:Cancel 比 Try 先执行(如 Try 超时后 Cancel 触发,延迟的 Try 才到达)
  • 解决:Try 前检查是否已 Cancel,若已 Cancel 则不再执行 Try
sql
-- TCC 事务记录表
CREATE TABLE tcc_transaction_log (
    xid           VARCHAR(128) PRIMARY KEY,
    branch_id     VARCHAR(128),
    status        VARCHAR(20),  -- TRY / CONFIRM / CANCEL / CANCEL_EMPTY
    business_key  VARCHAR(128),
    created_time  DATETIME
);

TCC 适用场景

  • 高性能要求、需要强隔离
  • 非数据库资源(如库存、积分)
  • 资金类业务(需保证不超扣)

14.5 ★★★Saga 模式的工作原理是什么?适合什么场景?补偿事务如何设计?

📖 点击查看答案

Saga 模式原理

  • 将长事务拆分为多个本地事务 T1, T2, ..., Tn
  • 每个本地事务 Ti 对应一个补偿事务 Ci
  • 正向执行:T1 → T2 → ... → Tn
  • 回滚执行:若 Tk 失败,执行 Ck-1 → Ck-2 → ... → C1

两种实现方式

1. 编排式(Choreography):去中心化,各服务监听事件触发下一步

java
// 订单服务
public void createOrder() {
    orderMapper.insert(order);
    eventBus.publish(new OrderCreatedEvent(order));  // 发布事件
}
// 库存服务监听
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
    stockMapper.deduct(event.getOrderId());
    eventBus.publish(new StockDeductedEvent(event.getOrderId()));
}
// 支付服务监听
@EventListener
public void onStockDeducted(StockDeductedEvent event) {
    try {
        payMapper.pay(event.getOrderId());
    } catch (Exception e) {
        eventBus.publish(new PayFailedEvent(event.getOrderId()));  // 触发补偿
    }
}

2. 协调式(Orchestration):中心化协调器管理流程

java
class SagaOrchestrator {
    void execute(Order order) {
        try {
            executeStep(() -> orderService.create(order));
            executeStep(() -> stockService.deduct(order));
            executeStep(() -> payService.pay(order));
            executeStep(() -> deliveryService.ship(order));
        } catch (Exception e) {
            compensate(order);  // 反向补偿
        }
    }
    void compensate(Order order) {
        tryStep(() -> deliveryService.cancelShip(order));
        tryStep(() -> payService.refund(order));
        tryStep(() -> stockService.restore(order));
        tryStep(() -> orderService.cancel(order));
    }
}

补偿事务设计要点

  1. 补偿必须是幂等的:可能被重试
  2. 补偿不能失败:需重试机制 + 人工兜底
  3. 补偿语义明确:如支付补偿是退款,不是删除支付记录
  4. 不可补偿的操作放最后:如发送邮件、发货等
  5. 隔离性问题:中间状态可见,需业务容忍或加锁

Saga 适用场景

  • 长流程业务(旅游预订、订单履约)
  • 涉及第三方系统(无法 XA)
  • 微服务编排
  • 业务可容忍中间不一致

Saga vs TCC

  • Saga:无 Try 阶段,直接执行真实操作,失败补偿,适合长流程
  • TCC:Try 预留资源,性能高但侵入性强,适合短事务

14.6 ★★★基于本地消息表的最终一致性方案是如何实现的?适用什么场景?

📖 点击查看答案

核心思想:利用本地事务保证业务操作和消息记录原子性,异步发送消息保证最终一致性。

实现流程

  1. 业务操作 + 插入本地消息表(同一本地事务)
  2. 定时任务扫描消息表中"未发送"的消息
  3. 调用下游服务(或投递 MQ)
  4. 下游消费成功后回调更新消息状态
  5. 失败重试,超过次数告警人工介入
java
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        // 同一事务插入消息表
        Message msg = new Message();
        msg.setBizId(order.getId());
        msg.setType("ORDER_CREATED");
        msg.setPayload(JSON.toJSONString(order));
        msg.setStatus("PENDING");
        msg.setRetryCount(0);
        messageMapper.insert(msg);  // 与业务同一事务
    }
}

// 定时任务扫描发送
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
    List<Message> msgs = messageMapper.selectByStatus("PENDING", 100);
    for (Message msg : msgs) {
        try {
            mqProducer.send(msg.getTopic(), msg.getPayload());
            messageMapper.updateStatus(msg.getId(), "SENT");
        } catch (Exception e) {
            messageMapper.incrementRetry(msg.getId());
            if (msg.getRetryCount() > MAX_RETRY) {
                messageMapper.updateStatus(msg.getId(), "FAILED");
                alertService.alert("消息发送失败: " + msg.getId());
            }
        }
    }
}

// 消费端幂等处理
@RocketMQMessageListener(topic = "ORDER_CREATED")
public class OrderConsumer implements RocketMQListener<String> {
    public void onMessage(String payload) {
        OrderEvent event = JSON.parseObject(payload, OrderEvent.class);
        if (exists(event.getBizId())) return;  // 幂等检查
        process(event);
    }
}

表设计

sql
CREATE TABLE local_message (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_id       VARCHAR(64),
    topic        VARCHAR(100),
    payload      TEXT,
    status       VARCHAR(20),  -- PENDING / SENT / CONFIRMED / FAILED
    retry_count  INT DEFAULT 0,
    next_retry   DATETIME,
    created_time DATETIME,
    updated_time DATETIME,
    INDEX idx_status_retry (status, next_retry)
);

适用场景

  • 异步解耦的最终一致性场景
  • 订单创建后通知积分、优惠券、物流
  • 对实时性要求不高的跨服务数据同步

优缺点

  • 优点:实现简单、可靠性高、不依赖 MQ 事务
  • 缺点:与业务耦合(消息表侵入)、定时扫描有延迟、高并发下消息表压力大

14.7 ★★★基于 RocketMQ 事务消息实现最终一致性的方案是如何工作的?与本地消息表方案相比有什么优劣?

📖 点击查看答案

RocketMQ 事务消息原理:利用半消息(Half Message)+ 事务回查机制保证本地事务与消息发送的原子性。

核心流程

  1. 发送半消息:Producer 发送半消息到 Broker,半消息对消费者不可见
  2. 执行本地事务:Producer 执行本地业务事务
  3. 提交/回滚
    • 本地事务成功 → 发送 Commit,半消息变为可消费
    • 本地事务失败 → 发送 Rollback,删除半消息
  4. 事务回查:若 Broker 未收到 Commit/Rollback(如网络问题),主动回调 Producer 的 checkLocalTransaction 查询本地事务状态
  5. 消费:消费者消费消息,幂等处理
java
// 事务消息生产者
public class OrderTransactionProducer {
    public void sendOrderMessage(Order order) {
        Message msg = new Message("ORDER_TOPIC", JSON.toJSONString(order).getBytes());
        TransactionMQProducer producer = new TransactionMQProducer("order_group");
        producer.setTransactionListener(new TransactionListener() {
            // 执行本地事务
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                try {
                    orderService.createOrder(order);  // 本地事务
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }
            // 事务回查(Broker 主动调用)
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                Order order = JSON.parseObject(msg.getBody(), Order.class);
                return orderService.exists(order.getId())
                    ? LocalTransactionState.COMMIT_MESSAGE
                    : LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });
        producer.sendMessageInTransaction(msg, null);
    }
}

与本地消息表对比

维度RocketMQ 事务消息本地消息表
业务侵入低(接口回调)高(需建消息表)
实现复杂度中(需实现回查)低(定时任务扫描)
实时性高(事务提交即发送)低(定时扫描有延迟)
数据库压力消息表压力大
依赖强依赖 RocketMQ任何 MQ 即可
回查机制内置需自己实现重试
适用场景RocketMQ 技术栈异构 MQ、老系统改造

注意事项

  • 事务消息只保证"本地事务与消息发送"的一致性,不保证消费成功(消费端需幂等)
  • 事务回查有次数限制(默认 15 次),超时后按规则处理
  • 半消息有存储成本,不适合高频小消息

14.8 ★★★各分布式事务方案如何选型(强一致 vs 最终一致)?Seata 的 AT、TCC、Saga、XA 四种模式分别适用于什么场景?

📖 点击查看答案

强一致 vs 最终一致选型

维度强一致(XA/2PC)最终一致(TCC/Saga/消息)
性能低(资源锁定)
可用性低(阻塞)
一致性最终
适用场景资金、库存核心交易互联网高并发业务
业务侵入中-高

选型决策树

  1. 是否涉及第三方系统?是 → Saga / 事务消息
  2. 是否为资金类强一致场景?是 → TCC / XA
  3. 是否关系型数据库 + 简单 CRUD?是 → Seata AT
  4. 是否长流程业务?是 → Saga
  5. 是否需要异步解耦?是 → 本地消息表 / 事务消息

Seata 四种模式适用场景

模式一致性侵入性性能适用场景
AT最终一致无(自动)关系型数据库、简单 CRUD、快速接入
TCC最终一致高(手写三阶段)资金/库存扣减、需强隔离、非 DB 资源
Saga最终一致中(补偿)长流程业务编排、第三方系统、旅游预订
XA强一致跨库强一致、并发低、金融核心
java
// Seata 模式选择示例
// AT:最常用,无侵入
@GlobalTransactional
public void transfer(Long from, Long to, BigDecimal amount) {
    accountMapper.debit(from, amount);
    accountMapper.credit(to, amount);
}

// TCC:高并发资金场景
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean tryDeduct(BusinessActionContext ctx, String account, BigDecimal amount) {
    accountMapper.freeze(account, amount);  // 冻结
    return true;
}

// Saga:长流程编排
@SagaOrchchestration
public void bookTrip(TripOrder order) {
    step(bookFlight).compensate(cancelFlight);
    step(bookHotel).compensate(cancelHotel);
    step(bookCar).compensate(cancelCar);
    step(payment).compensate(refund);
}

// XA:跨库强一致
@GlobalTransactional(transactionType = GlobalTransactionType.XA)
public void crossDbUpdate() {
    db1Mapper.update(...);  // xa_db1
    db2Mapper.update(...);  // xa_db2
}

实战建议

  1. 优先 AT 模式(零侵入),满足 80% 场景
  2. 资金/库存场景用 TCC(隔离性好)
  3. 长流程编排用 Saga
  4. XA 仅在强一致刚需时使用
  5. 异步场景用本地消息表/事务消息
  6. 同一系统可混合使用多种模式

基于 4 份主流面试题库整合精选,共 200 题