小陈的知识图谱
系统设计L3 深入核心重点

分布式理论

CAP、BASE、一致性协议(Paxos/Raft)、分布式事务

分布式理论(Distributed Theory)

分布式系统是指一组通过网络通信、协同工作的计算机节点,对外表现为一个整体。分布式理论是解决分布式系统一致性问题的基础,也是系统设计面试的必考内容。

---

一、CAP 理论

CAP 理论是分布式系统的基石,由 Eric Brewer 在 2000 年提出。它指出一个分布式系统最多只能同时满足以下三个特性中的两个:

一致性(C)
                 ●
                / \
               /   \
              /     \
             /       \
            /         \
           /           \
          /             \
         /               \
        ●─────────────────●
    可用性(A)           分区容错性(P)

三个特性详解

特性英文含义
一致性Consistency所有节点在同一时刻看到相同的数据
可用性Availability每个请求都能得到响应(成功或失败)
分区容错性Partition Tolerance系统能容忍网络分区(节点间通信中断)

CAP 的权衡

在实际分布式系统中,网络分区是不可避免的(交换机故障、网线断裂等),因此 P 是必须保证的。我们实际上是在 C 和 A 之间做选择:

CP 系统                    AP 系统
    ┌──────────────────┐     ┌──────────────────┐
    │                  │     │                  │
    │  发生网络分区时    │     │  发生网络分区时    │
    │                  │     │                  │
    │ 暂停非多数分区的  │     │  所有分区继续     │
    │  服务(放弃可用性)│     │  提供服务         │
    │                  │     │  (返回不一致数据) │
    │ 例子:ZooKeeper  │     │  例子:Eureka     │
    │      etcd        │     │       DNS        │
    └──────────────────┘     └──────────────────┘

CA 系统:单机数据库(MySQL 单实例),但实际上单机不涉及网络分区,严格来说不能算分布式系统。

系统选择说明
ZooKeeperCP发生分区时,少数派节点停止服务
EurekaAP发生分区时,各节点继续服务(返回可能过期的数据)
NacosCP/AP 切换根据配置选择 CP 或 AP 模式
Redis ClusterCP发生分区时,少数派分区不可用
CassandraAP发生分区时,各分区继续服务

面试重点

CAP 不能简单理解为"3选2"。实际设计中:

  • 正常运行时,C 和 A 可以同时满足
  • 只有发生网络分区时,才需要在 C 和 A 之间做选择
  • 大多数业务场景优先保证 AP,通过其他手段达成最终一致性

---

二、BASE 理论

BASE 理论是对 CAP 中 AP 方案的延伸,由 eBay 的架构师 Dan Pritchett 提出。

缩写全称含义
BABasically Available基本可用:系统允许降级,比如秒杀时关闭非核心功能
SSoft State软状态:允许系统存在中间状态(不一致)
EEventually Consistent最终一致性:经过一段时间后,数据会达到一致

基本可用(BA)的体现

正常情况:
┌─────────────────────────────────────────────┐
│  用户查询:返回最新数据                      │
│  下单功能:正常                             │
│  推荐功能:正常                             │
└─────────────────────────────────────────────┘

秒杀期间(降级):
┌─────────────────────────────────────────────┐
│  用户查询:返回缓存数据(可能延迟几秒)       │
│  下单功能:正常(核心功能不可降级)           │
│  推荐功能:关闭(非核心,降级处理)           │
└─────────────────────────────────────────────┘

最终一致性(E)的实现方案

1. 异步通知:订单创建后发送 MQ 消息,库存服务异步扣减

2. 定期校对:定时任务扫描不一致数据并修复

3. 补偿机制:失败后通过重试或回滚达到最终一致

面试要点:CAP 是理论约束,BASE 是实践指导。互联网公司的大多数系统都是 BASE 导向的。

---

三、一致性协议

3.1 Raft 协议

Raft 是分布式一致性协议中最易于理解的一个,它将问题分解为三个子问题:

  • Leader 选举(Leader Election)
  • 日志复制(Log Replication)
  • 安全性(Safety)

Leader 选举

初始状态:所有节点都是 Follower
     ┌──────┐    ┌──────┐    ┌──────┐
     │Follower│  │Follower│  │Follower│
     │Term 1  │  │Term 1  │  │Term 1  │
     └──────┘    └──────┘    └──────┘

     选举超时 → Follower 变成 Candidate
     ┌──────┐    ┌──────┐    ┌──────┐
     │Candidate│ │Follower│  │Follower│
     │Term 2   │ │Term 2  │  │Term 2  │
     └──────┘    └──────┘    └──────┘

     获得多数票 → Candidate 变成 Leader
     ┌──────┐    ┌──────┐    ┌──────┐
     │Leader  │  │Follower│  │Follower│
     │Term 2  │  │Term 2  │  │Term 2  │
     └──────┘    └──────┘    └──────┘

选举流程

1. Follower 在选举超时内未收到 Leader 心跳,转为 Candidate

2. Candidate 自增 Term(任期),给自己投票,并向其他节点发送 RequestVote RPC

3. 获得超过半数节点投票的 Candidate 成为 Leader

4. Leader 定期发送心跳(AppendEntries RPC)维持权威

5. 如果多个 Candidate 分裂选票(没有多数),开启新一轮选举

随机选举超时(150ms-300ms)防止选票分裂。

日志复制

客户端请求
    │
    ▼
┌──────────┐
│  Leader   │ ← 接收客户端请求
│          │
│  Entry 1  │ ← 追加到本地日志
│  Entry 2  │
└─────┬────┘
      │ AppendEntries RPC
      ├───────────────────────┐
      ▼                       ▼
┌──────────┐          ┌──────────┐
│ Follower1│          │ Follower2│
│          │          │          │
│ Entry 1  │          │ Entry 1  │
│ Entry 2  │          │ Entry 2  │
└──────────┘          └──────────┘

Leader 收到多数确认 → 提交 Entry → 应用到状态机

日志复制流程

1. Leader 接收客户端请求,追加日志条目到本地日志

2. Leader 并行向所有节点发送 AppendEntries RPC

3. Follower 接收日志并追加到本地日志,返回成功

4. Leader 收到超过半数节点的成功响应后,提交日志(应用到状态机)

5. Leader 通知 Follower 该日志已提交

3.2 Paxos 协议

Paxos 是分布式一致性协议的鼻祖,比 Raft 更难理解。它定义了三类角色:

角色职责
Proposer(提议者)提出提案(value)
Acceptor(接受者)对提案进行投票
Learner(学习者)学习已批准的提案

Paxos 两阶段流程

阶段一:Prepare(准备)
  Proposer → 所有 Acceptor:Prepare(N)
  Acceptor:如果 N > 之前见过的最大编号,承诺不再接受小于 N 的提案
  Acceptor → Proposer:Promise(N, 已接受的提案编号和值)

阶段二:Accept(接受)
  Proposer:收到多数 Promise,选择最大编号提案的值(如果没有则用自己提案的值)
  Proposer → 所有 Acceptor:Accept(N, value)
  Acceptor:接受提案(除非已承诺更大的编号)
  Acceptor → Proposer:Accepted(N, value)

Paxos vs Raft 对比

对比项PaxosRaft
理解难度相对简单
角色Proposer/Acceptor/LearnerLeader/Follower/Candidate
投票机制基本 Paxos 需要多轮投票领导者简化了投票
实际应用理论基石ZooKeeper(ZAB 类似 Raft)、etcd、Consul

3.3 ZAB 协议(ZooKeeper Atomic Broadcast)

ZAB 是 ZooKeeper 使用的一致性协议,分为两个模式:

崩溃恢复模式(Recovery):
  Leader 宕机 → 选举新 Leader → 同步数据 → 进入广播模式

消息广播模式(Broadcast):
  Leader 接收写请求 → 广播 Proposal → 多数 Ack → 提交 Commit

Epoch(纪元):ZAB 中的 Term 概念,标识 Leader 的任期。

---

四、分布式事务方案

4.1 方案对比总览

方案一致性性能代码侵入适用场景
2PC强一致低(同步阻塞)短事务、低并发
TCC强一致高(需实现 Try/Confirm/Cancel)跨服务、高并发
本地消息表最终一致异步场景、低一致性要求
可靠消息(MQ)最终一致异步场景
Seata AT强一致低(无侵入)微服务场景

4.2 2PC(两阶段提交)

阶段一:准备(Prepare Phase)
  协调者                           参与者
    │                                │
    ├─── CanCommit? ────────────────→│
    │←── Yes / No ──────────────────┤
    │                                │
阶段二:提交/回滚(Commit/Abort Phase)
    │                                │
    ├─── DoCommit / DoAbort ────────→│
    │←── Commit ACK / Abort ACK ────┤

// 伪代码示例:XA 事务
@Transactional
public void createOrder(Order order) {
    // 资源管理器1:订单库
    orderDao.insert(order);
    // 资源管理器2:库存库(跨库 XA 事务)
    stockDao.deduct(order.getSkuId(), order.getQuantity());
    // 资源管理器3:账户库
    accountDao.debit(order.getUserId(), order.getAmount());
    // 提交时执行 2PC 协议
}

缺点

  • 同步阻塞:参与者必须等待协调者指令
  • 单点故障:协调者宕机可能导致所有参与者阻塞
  • 数据不一致:第二阶段部分参与者提交失败

4.3 TCC(Try-Confirm-Cancel)

TCC 是业务层面的补偿型分布式事务方案。

Try 阶段:
  - 订单服务:创建订单,状态为"待确认"
  - 库存服务:预留库存(冻结库存 + 1)
  - 账户服务:冻结金额

Confirm 阶段(Try 全部成功):
  - 订单服务:订单状态改为"已确认"
  - 库存服务:扣减实际库存(释放冻结库存)
  - 账户服务:扣减账户余额(释放冻结金额)

Cancel 阶段(Try 任何失败):
  - 订单服务:取消订单
  - 库存服务:释放预留库存
  - 账户服务:解冻金额

public interface OrderTccService {

    @TwoPhaseBusinessAction(
        name = "orderTcc",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    void tryCreateOrder(BusinessActionContext context, Order order);

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

// 使用 TCC 框架(如 tcc-transaction、Hmily)
@Service
public class OrderServiceImpl {

    @Autowired
    private OrderTccService orderTccService;
    @Autowired
    private InventoryTccService inventoryTccService;
    @Autowired
    private AccountTccService accountTccService;

    public void createOrder(Order order) {
        try {
            orderTccService.tryCreateOrder(null, order);
            inventoryTccService.tryDeduct(null, order.getSkuId(), order.getQuantity());
            accountTccService.tryFreeze(null, order.getUserId(), order.getAmount());
        } catch (Exception e) {
            // 任一个 Try 失败,框架自动调用各服务的 Cancel
            throw new RuntimeException("创建订单失败", e);
        }
    }
}

4.4 本地消息表

┌──────────────────────────────────────────────────────┐
│                                                      │
│  订单服务                    库存服务                  │
│    │                           │                     │
│    │ 1. 创建订单 + 插入消息表   │                     │
│    │   (同一个本地事务)         │                     │
│    │                           │                     │
│    │ 2. 定时任务扫描消息表      │                     │
│    ├───────────────────────────→│ 3. 发送 MQ 消息     │
│    │                           │                     │
│    │ 4. 库存服务消费并扣减库存  │                     │
│    │                           │                     │
│    │ 5. 扣减成功 → 更新消息状态 │                     │
│    │   扣减失败 → 定时重试     │                     │
│    │                           │                     │
└──────────────────────────────────────────────────────┘

-- 本地消息表
CREATE TABLE local_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    business_key VARCHAR(64) NOT NULL,    -- 业务唯一键
    business_module VARCHAR(32) NOT NULL, -- 业务模块
    message_body TEXT NOT NULL,           -- 消息内容(JSON)
    status TINYINT NOT NULL DEFAULT 0,    -- 0=待发送 1=已发送 2=已完成
    retry_count INT DEFAULT 0,            -- 重试次数
    next_retry_time DATETIME,             -- 下次重试时间
    create_time DATETIME NOT NULL,
    update_time DATETIME NOT NULL,
    UNIQUE KEY uk_business_key (business_key)
);

// 本地消息表 + 定时任务实现最终一致性
@Component
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // 1. 创建订单
        orderDao.insert(order);
        // 2. 插入本地消息表(同一个本地事务)
        LocalMessage msg = new LocalMessage();
        msg.setBusinessKey(order.getId().toString());
        msg.setBusinessModule("order");
        msg.setMessageBody(JSON.toJSONString(order));
        msg.setStatus(0);
        localMessageDao.insert(msg);
    }
}

// 定时任务:扫描待发送的消息
@Component
public class MessageRetryTask {

    @Scheduled(fixedDelay = 5000)  // 每5秒扫描一次
    public void scanAndSend() {
        List<LocalMessage> pendingMessages = localMessageDao.selectPending();
        for (LocalMessage msg : pendingMessages) {
            try {
                // 发送到 MQ
                mqProducer.send(msg.getMessageBody());
                // 更新状态
                localMessageDao.updateStatus(msg.getId(), 1);
            } catch (Exception e) {
                msg.setRetryCount(msg.getRetryCount() + 1);
                msg.setNextRetryTime(DateUtil.addMinutes(new Date(), 5));
                localMessageDao.update(msg);
            }
        }
    }
}

4.5 Seata AT 模式

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务框架,AT 模式对业务代码几乎无侵入

Seata AT 执行流程:
                    TC(Transaction Coordinator)
                    /        |        \
                   /         |         \
                  /          |          \
              TM(业务)    RM(资源1)    RM(资源2)

1. TM 向 TC 申请全局事务 ID(XID)
2. TM 通过 XID 传播到各个微服务
3. 每个 RM 执行业务 SQL,同时记录前后镜像(undo log)
4. TM 向 TC 发起全局提交/回滚
5. TC 通知各 RM 提交/回滚
   - 提交:删除 undo log
   - 回滚:根据 undo log 生成反向 SQL 恢复数据

// Seata AT 对业务几乎无侵入
@GlobalTransactional  // 只需加这个注解
public void createOrder(Order order) {
    // 订单服务本地事务
    orderDao.insert(order);

    // 调用库存服务(跨服务)
    inventoryFeignClient.deduct(order.getSkuId(), order.getQuantity());

    // 调用账户服务(跨服务)
    accountFeignClient.debit(order.getUserId(), order.getAmount());

    // 所有服务都成功 → 全局提交
    // 任一服务失败 → 全局回滚(根据 undo log 自动恢复)
}

---

五、面试高频问题

Q1:CAP 中的 P 为什么是必须的?

因为网络分区是分布式系统的必然现象。消息丢失、交换机故障、网络延迟等都可能导致节点间通信中断。

Q2:你们项目用了什么分布式事务方案?

根据业务场景选择:核心交易用 TCC 或 Seata AT(强一致),非核心场景用 RocketMQ 事务消息(最终一致)。

Q3:Raft 选举时出现平票怎么办?

每个 Candidate 的选举超时时间是随机的(150ms-300ms),大概率会有先发起的 Candidate 获得多数票。如果确实出现分裂,本次选举失败,等待下一轮随机超时后重新选举。

Q4:2PC 有什么问题?为什么不用?

同步阻塞(性能差)、协调者单点、数据不一致(第二阶段部分失败)。现代互联网高并发场景下基本不用 2PC。

---

六、面试小贴士

1. CAP 理论是所有分布式系统面试的起点,一定要能结合具体系统举例(ZooKeeper=CP, Eureka=AP)

2. BASE 理论是互联网公司的指导思想,"最终一致性"是核心关键词

3. Paxos vs Raft 面试中至少能说清楚 Raft 的选举和日志复制流程

4. 分布式事务面试时不要说"我们只用一种方案",电商系统往往是多方案组合

5. 面试中可以说"我们核心链路用 Seata AT 保证强一致,非核心用 MQ 保证最终一致"

核心要点

  • CAP 理论与 BASE 理论
  • Raft 协议核心流程
  • 分布式事务方案对比
  • Seata AT 模式原理
  • Paxos vs Raft vs ZAB