文章

分布式理论与一致性协议面试题 —— 从 CAP 定理到 Raft 共识的深度问答

覆盖CAP/BASE理论、Paxos/Multi-Paxos、Raft(选举/日志复制/安全性)、ZAB协议、分布式事务(2PC/3PC/TCC/Saga)、Gossip协议、向量时钟、etcd/ZooKeeper对比,25道高频题附架构图

分布式理论与一致性协议面试题 —— 从 CAP 定理到 Raft 共识的深度问答

分布式系统是后端架构的核心难点——CAP 定理决定了系统设计的取舍方向,共识算法是分布式一致性的基石,分布式事务是业务落地的关键。面试中能讲清 Raft 的选举细节、2PC 与 TCC 的本质区别、Gossip 的收敛性分析,展示的是对分布式系统的理论深度和工程理解

这篇文章从基础理论 → 共识算法 → 分布式事务 → 分布式协调 → 进阶问题五条线展开,每道题都带架构图和方案对比

📌 关联阅读:系统设计面试题 · 消息队列面试题 · 数据库面试题


第一部分:分布式基础理论

Q1:什么是 CAP 定理?为什么不能三者兼得?

记忆点C一致 A可用 P分区容忍,三选二但 P 必选,实际是 CP 或 AP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CAP 定理:
  ┌─────────────────────────────────────┐
  │         分布式系统三要素              │
  │                                     │
  │    C (Consistency)   一致性          │
  │    所有节点同一时刻看到相同数据        │
  │         ╱         ╲                 │
  │        ╱    P必选   ╲               │
  │       ╱   网络分区    ╲             │
  │      CP     必然发生     AP          │
  │    ╱                      ╲         │
  │  A (Availability)  P (Partition)    │
  │  每个请求都能收到    网络分区时        │
  │  非错误响应         系统仍能运行      │
  └─────────────────────────────────────┘

为什么三选二?
  网络分区发生时(P 必选):
  ┌──────┐    网络断开    ┌──────┐
  │Node A│ ──── ✕ ────── │Node B│
  └──────┘               └──────┘

  选 CP:拒绝写入 → 保证一致性,牺牲可用性
  选 AP:允许读写 → 保证可用性,牺牲一致性
选择含义代表系统
CP分区时拒绝不一致的请求ZooKeeper、etcd、HBase、MongoDB
AP分区时允许返回旧数据Cassandra、DynamoDB、Eureka
CA不考虑分区(单机才行)传统单机 MySQL

面试加分点:CAP 中的 C 是线性一致性(Linearizability),比数据库 ACID 的 C(约束一致性)更强。实际工程中更常用 BASE 来做折中。


Q2:BASE 理论是什么?和 ACID 有什么区别?

记忆点ACID 强一致刚性事务,BASE 最终一致柔性事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ACID vs BASE:

ACID(刚性事务):             BASE(柔性事务):
┌────────────────────┐      ┌────────────────────────┐
│ A - Atomicity      │      │ BA - Basically Available│
│     原子性          │      │      基本可用            │
│ C - Consistency    │      │ S  - Soft State         │
│     一致性          │      │      软状态              │
│ I - Isolation      │      │ E  - Eventually         │
│     隔离性          │      │      Consistent          │
│ D - Durability     │      │      最终一致性           │
│     持久性          │      │                         │
├────────────────────┤      ├────────────────────────┤
│ 强一致 → 高延迟     │      │ 最终一致 → 高可用       │
│ 单机/小规模         │      │ 大规模分布式             │
└────────────────────┘      └────────────────────────┘
维度ACIDBASE
一致性强一致(实时)最终一致(延迟)
可用性可能阻塞基本可用
适用场景银行转账电商库存/社交点赞
实现复杂度低(数据库保证)高(应用层补偿)
性能低(锁等待)高(异步)

面试加分点:BASE 不是不要一致性,而是允许中间状态(Soft State),通过异步补偿最终达到一致。实际系统中大多数场景都是 BASE,只有资金等核心场景才用 ACID。


Q3:一致性模型有哪些?强一致、顺序一致、最终一致有什么区别?

记忆点线性 > 顺序 > 因果 > 最终,越强性能越差

1
2
3
4
5
6
7
8
9
10
11
12
13
一致性模型强弱排列:

强 ←───────────────────────────────────→ 弱

线性一致性        顺序一致性      因果一致性      最终一致性
Linearizability  Sequential    Causal        Eventual

全局实时序        全局某种序      因果序          无序保证
所有操作像        所有操作有      有因果关系      最终相同
在一个节点        一个全序        的操作有序

etcd/ZooKeeper   ZooKeeper读    CRDT          Cassandra
                                             DynamoDB
模型定义代价典型系统
线性一致性操作像在单机上执行,有全局实时序性能最低etcd、Spanner
顺序一致性所有操作有一个全局序,但不要求实时较高ZooKeeper
因果一致性有因果关系的操作保序中等MongoDB(因果会话)
最终一致性停止写入后最终所有副本一致性能最高Cassandra、DNS

面试加分点:区分客户端视角和系统视角——读己所写(Read Your Writes)、单调读(Monotonic Reads)是客户端一致性保证,可以在最终一致性系统上通过 sticky session 实现。


Q4:什么是 Quorum 机制?NWR 策略怎么调?

记忆点W+R > N 保证能读到最新值,N=3 时常用 W2R2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Quorum NWR 机制:
  N = 副本总数
  W = 写成功需要的副本数
  R = 读需要查询的副本数

  当 W + R > N 时,读写集合必有交集 → 能读到最新值

  N=3, W=2, R=2 的例子:
  ┌──────┐  ┌──────┐  ┌──────┐
  │Node 1│  │Node 2│  │Node 3│
  │ v=2  │  │ v=2  │  │ v=1  │
  └──────┘  └──────┘  └──────┘
    写成功✓    写成功✓    未同步

  读 2 个节点,至少能读到一个 v=2 → 取最新版本
配置特点场景
W=N, R=1强写,读快读多写少
W=1, R=N弱写,读慢但一致写多读少
W=N/2+1, R=N/2+1平衡通用场景
W=1, R=1最快但无一致性保证允许脏读

面试加分点:Quorum 只保证能读到最新值,不保证线性一致性。还需要 read-repair 或 anti-entropy 机制来修复不一致的副本。DynamoDB 的 Sloppy Quorum 允许临时节点参与,提高可用性但减弱一致性。


Q5:什么是脑裂?如何防止?

记忆点网络分区导致多主,用奇数节点+过半选举防止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
脑裂场景:
  正常状态:
  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
  │Node 1│──│Node 2│──│Node 3│──│Node 4│──│Node 5│
  │Leader│  │Follow│  │Follow│  │Follow│  │Follow│
  └──────┘  └──────┘  └──────┘  └──────┘  └──────┘

  网络分区后:
  ┌──────────────┐     ┌──────────────────────┐
  │ 分区 A (2节点)│     │ 分区 B (3节点)         │
  │ Node1 Node2  │     │ Node3 Node4 Node5    │
  │ "我是Leader" │     │ 选出新 Leader(Node3)  │
  └──────────────┘     └──────────────────────┘
       ↓                        ↓
  少数派,无法提交          多数派,正常服务
  (2 < 5/2+1=3)         (3 ≥ 3 ✓)
防脑裂机制原理使用系统
过半选举Leader 需多数票,分区后最多一个分区有多数Raft、ZAB
奇数节点避免恰好对半分etcd (3/5/7)
Fencing Token旧 Leader 的写操作被存储层拒绝Chubby
租约(Lease)Leader 持有有时效的锁,过期自动卸任etcd

面试加分点:过半机制的本质是 2 * Quorum > N,确保任意两个 Quorum 有交集。这也是为什么分布式系统推荐奇数节点(3比4容忍的故障数相同,但成本更低)。


第二部分:共识算法

Q6:Paxos 算法的核心思想是什么?

记忆点两阶段——Prepare 抢编号,Accept 提值,多数派同意即通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Basic Paxos 两阶段流程:

角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者)

阶段一:Prepare
  Proposer                    Acceptor A/B/C
     │── Prepare(n=1) ──────→ │ │ │
     │                        │ │ │ 检查:n > 已承诺编号?
     │←── Promise(n=1,无值) ── │ │ │ 是→承诺不再接受<n的提案
     │←── Promise(n=1,无值) ── │ │ │
     │    (多数派响应)          │ │ │

阶段二:Accept
     │── Accept(n=1,v="X") ──→ │ │ │
     │                         │ │ │ 检查:没承诺过>n的提案?
     │←── Accepted(n=1,v="X")──│ │ │ 是→接受提案
     │←── Accepted(n=1,v="X")──│ │ │
     │    (多数派接受 → 提案通过) │ │ │
阶段目的Acceptor 规则
Prepare(n)抢占编号 n承诺不再响应编号 < n 的提案
Promise返回已接受的最高编号提案如已接受过提案,返回(n_a, v_a)
Accept(n,v)提交值 v未承诺过更高编号则接受
Accepted确认接受多数派接受 → 值被选定(chosen)

关键约束:如果 Proposer 在 Prepare 阶段收到了已被接受的值,必须使用该值(而非自己的值)发起 Accept,这保证了已选定的值不会被覆盖。

面试加分点:Basic Paxos 的活锁问题——两个 Proposer 交替 Prepare 导致互相打断。解决方案是选一个 Leader 做唯一 Proposer(即 Multi-Paxos)。


Q7:Multi-Paxos 做了哪些优化?和 Basic Paxos 有什么区别?

记忆点选 Leader 省 Prepare,一次 Prepare 多次 Accept

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Basic Paxos vs Multi-Paxos:

Basic Paxos(每个值都要两阶段):
  值1: Prepare → Promise → Accept → Accepted
  值2: Prepare → Promise → Accept → Accepted
  值3: Prepare → Promise → Accept → Accepted
  每个值 2轮 RPC = 6轮 RPC

Multi-Paxos(一次 Prepare,多次 Accept):
  选Leader: Prepare → Promise  (一次性)
  值1: Accept → Accepted
  值2: Accept → Accepted
  值3: Accept → Accepted
  1次Prepare + 3次Accept = 4轮 RPC

  ┌────────┐    Accept(v)    ┌──────────┐
  │ Leader │ ──────────────→ │ Acceptor │ × 多数派
  │(唯一   │ ←───────────── │          │
  │Proposer)│   Accepted     └──────────┘
  └────────┘
对比Basic PaxosMulti-Paxos
Prepare每个值都要Leader 选好后省略
延迟2 轮 RPC/值1 轮 RPC/值
活锁可能不会(唯一 Proposer)
Leader
实际系统教学用Chubby、Spanner

面试加分点:Multi-Paxos 论文没有明确规定 Leader 选举方式,各实现自行设计。这也是 Paxos 难以实现的原因之一,Raft 正是为了解决这个问题而设计的。


Q8:Raft 算法的核心流程是什么?

记忆点Leader选举 + 日志复制 + 安全性,任期(Term)是逻辑时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Raft 三个子问题:

1. Leader 选举
   ┌──────────┐  超时  ┌──────────┐  获得多数票  ┌──────────┐
   │ Follower │──────→│ Candidate│───────────→│  Leader  │
   └──────────┘       └──────────┘            └──────────┘
        ↑                  │ 发现更高 Term           │
        │                  │ 或选举超时重来           │ 发现更高 Term
        │                  ↓                        │
        └──────────────────────────────────────────┘

2. 日志复制
   Client → Leader → AppendEntries RPC → Followers
                  ← 多数派确认 ←
            提交(commit) → 应用到状态机

3. 状态:
   Term 1          │  Term 2          │  Term 3
   Leader: Node A  │  Leader: Node B  │  Leader: Node C
   ─────────────────────────────────────────────→ 时间
状态规则
Follower响应 Leader/Candidate 的 RPC,超时变 Candidate
Candidate自增 Term,投票给自己,发 RequestVote
Leader发心跳阻止选举,处理客户端请求

Term(任期)规则

  • 每个节点维护当前 Term
  • Term 充当逻辑时钟,识别过期 Leader
  • 收到更高 Term → 立即变回 Follower
  • 收到更低 Term 的请求 → 拒绝

面试加分点:Raft 的随机选举超时(150-300ms)是避免选举冲突的关键设计——不同节点超时时间不同,先超时的先发起选举,大概率一轮就选出 Leader。


Q9:Raft 的日志复制机制是怎样的?什么是 commitIndex?

记忆点Leader 追加 → 多数确认 → 提交 → 应用状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
日志复制流程:

Client 请求 SET x=1:
  ┌────────┐  1.追加日志    ┌─────────────────────────┐
  │ Client │──────────────→│ Leader (Node A)          │
  └────────┘               │ Log: [1:SET x=1]         │
                           └──────┬──────────────────┘
                     2.AppendEntries│RPC
                    ┌──────────────┼──────────────┐
                    ↓              ↓              ↓
              ┌──────────┐  ┌──────────┐  ┌──────────┐
              │ Follower │  │ Follower │  │ Follower │
              │ (Node B) │  │ (Node C) │  │ (Node D) │
              │ 复制成功✓ │  │ 复制成功✓ │  │ 网络慢... │
              └──────────┘  └──────────┘  └──────────┘
                    │              │
                    └──────────────┘
                      3. 多数派(3/5)确认
                         ↓
                    4. commitIndex 推进
                       Leader 应用到状态机
                         ↓
                    5. 下次心跳通知 Follower 提交

日志结构:
  Index:  1       2       3       4       5
  Term:   1       1       1       2       2
  Cmd:  SET x=1 SET y=2 DEL z  SET x=3 SET w=4
                         ↑               ↑
                    commitIndex      最新日志
概念含义
Log Entry(Term, Index, Command) 三元组
commitIndex已知被多数派复制的最高日志索引
lastApplied已应用到状态机的最高索引
nextIndex[]Leader 为每个 Follower 维护的下一条发送索引
matchIndex[]Leader 已知每个 Follower 复制到的最高索引

面试加分点:Leader 只能提交当前 Term 的日志(Raft 论文 Figure 8 问题)。如果 Leader 直接提交旧 Term 的日志,可能导致已提交日志被覆盖。解决方案:通过提交当前 Term 的新日志间接提交旧日志。


Q10:Raft 如何保证安全性?选举限制有哪些?

记忆点日志最新才能当选,已提交的日志永不丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
选举限制(Election Restriction):

投票规则——Candidate 的日志必须"至少和我一样新":
  比较方式:
  1. 先比最后一条日志的 Term,Term 大的更新
  2. Term 相同则比 Index,Index 大的更新

  Node A 日志: [T1:1] [T1:2] [T2:3]         ← 更新 ✓
  Node B 日志: [T1:1] [T1:2]                 ← 旧
  Node C 日志: [T1:1] [T1:2] [T1:3] [T1:4]  ← Term 低

  A 向 B 拉票:B 的最后 Term=1 < A 的 Term=2 → 投票 ✓
  C 向 A 拉票:C 的最后 Term=1 < A 的 Term=2 → 拒绝 ✗

安全性保证:
  ┌──────────────────────────────────────────────┐
  │ 如果某条日志在某个 Term 被提交,               │
  │ 那么所有更高 Term 的 Leader 必然包含该日志。    │
  │                                              │
  │ 证明:                                        │
  │ - 被提交 → 多数派有该日志                      │
  │ - 新 Leader 需多数派投票                       │
  │ - 两个多数派必有交集                           │
  │ - 交集节点不会投票给日志更旧的 Candidate        │
  │ → 新 Leader 必有该日志 ∎                       │
  └──────────────────────────────────────────────┘
安全性属性保证
Leader 完整性已提交日志必在未来 Leader 中
日志匹配相同 Index+Term → 相同命令
Leader Append-OnlyLeader 不删不改已有日志
状态机安全相同 Index 应用相同命令

面试加分点:Raft 论文共识正确性的关键是 Leader Completeness Property。配合成员变更时的 Joint Consensus(联合共识),确保变更过程中不会产生两个 Leader。


Q11:Raft 和 Paxos 有什么区别?为什么说 Raft 更工程化?

记忆点Paxos 理论优雅但难实现,Raft 可理解性优先

对比PaxosRaft
设计目标理论完备性可理解性
LeaderMulti-Paxos 才有核心设计
日志可乱序提交严格顺序提交
成员变更未明确规定Joint Consensus
实现难度极高(论文到工程鸿沟大)低(论文即可实现)
学术论文1990 年 Lamport2014 年 Stanford
工业实现Chubby、Spanneretcd、TiKV、CockroachDB
1
2
3
4
5
6
7
8
9
10
11
Paxos 的工程困难:
  论文描述 → 补充细节 → 优化性能 → 工业实现
    ↓           ↓          ↓          ↓
  很清晰      论文没说    论文没说    各家不同
                                   (Chubby论文都说Paxos难)

Raft 的工程友好:
  论文描述 → 伪代码完整 → 直接实现 → 工业使用
    ↓           ↓          ↓          ↓
  很清晰      Figure 2    可参考     etcd/TiKV
              就够了      参考实现

面试加分点:Raft 牺牲了一些灵活性换取可理解性——比如不允许日志空洞(Paxos 允许),但这让实现和调试简单得多。Diego Ongaro 的博士论文做了用户实验证明 Raft 比 Paxos 更容易理解。


Q12:ZAB 协议是什么?和 Raft 有什么区别?

记忆点ZAB 是 ZooKeeper 专用协议,崩溃恢复+消息广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ZAB 两个模式:

1. 崩溃恢复(选主):
   Leader 挂了 → 选 ZXID 最大的节点 → 数据同步 → 切换到广播模式

2. 消息广播(正常工作):
   Client → Leader → Proposal(zxid) → Follower
                  ← ACK ←
            多数派ACK → Commit → Follower

ZXID 结构(64位):
  高32位: epoch(类似 Raft Term)
  低32位: counter(事务计数器)
  ┌──────────────────┬──────────────────┐
  │   epoch (32bit)  │  counter (32bit) │
  └──────────────────┴──────────────────┘
  新 Leader 上任 → epoch+1, counter=0
对比RaftZAB
设计目标通用共识ZooKeeper 原子广播
逻辑时钟Termepoch
日志标识(Term, Index)ZXID (epoch, counter)
选举依据日志最新ZXID 最大
数据同步Leader 发送缺失日志四种同步策略(DIFF/TRUNC/SNAP/全量)
读一致性需 ReadIndex/LeaseRead默认非线性读(sync 命令才是)

面试加分点:ZAB 的 TRUNC 策略可以截断 Follower 多余的日志(Leader 没有但 Follower 有的情况),这在旧 Leader 崩溃但日志未提交时发生。


第三部分:分布式事务

Q13:2PC(两阶段提交)的流程是什么?有什么问题?

记忆点准备阶段全票通过才提交,协调者单点 + 阻塞是硬伤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2PC 流程:

阶段一:Prepare(投票)
  ┌─────────────┐
  │ Coordinator │
  │  (协调者)    │
  └──────┬──────┘
   Prepare│请求
  ┌──────┼──────────────┐
  ↓      ↓              ↓
┌────┐ ┌────┐        ┌────┐
│ PA │ │ PB │        │ PC │
│Yes │ │Yes │        │Yes │  ← 所有参与者锁定资源,返回 Yes/No
└────┘ └────┘        └────┘

阶段二:Commit/Rollback
  全部 Yes → Coordinator 发 Commit
  任一 No  → Coordinator 发 Rollback

  ┌─────────────┐
  │ Coordinator │
  └──────┬──────┘
   Commit│
  ┌──────┼──────────────┐
  ↓      ↓              ↓
┌────┐ ┌────┐        ┌────┐
│ PA │ │ PB │        │ PC │
│ OK │ │ OK │        │ OK │  ← 执行提交,释放资源
└────┘ └────┘        └────┘
问题说明后果
同步阻塞Prepare 后参与者持锁等待性能差
协调者单点协调者挂了,参与者一直阻塞资源永远锁定
数据不一致Commit 消息部分发送后协调者挂部分提交部分未提交
网络分区参与者收不到决定无法自主决策

面试加分点:MySQL 的 XA 事务就是 2PC 的实现,但因为性能问题(锁持有时间长),互联网系统基本不用 XA,而是用 TCC 或 Saga。


Q14:3PC 解决了 2PC 的什么问题?为什么实际很少用?

记忆点加了 PreCommit 阶段减少阻塞,但网络分区仍有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3PC vs 2PC:

2PC:  Prepare ─────────→ Commit/Abort
      (锁定资源等待)      (持锁时间长)

3PC:  CanCommit → PreCommit → DoCommit
      (询问能否)   (预提交)    (正式提交)
      不锁资源     锁定资源    释放资源
                  有超时机制

3PC 的改进:
  ┌─────────────────────────────────────────────┐
  │ CanCommit:轻量询问,不锁资源                  │
  │ PreCommit:确认锁定,设置超时                   │
  │ DoCommit:正式提交                             │
  │                                               │
  │ 关键改进:参与者超时后可自主提交(减少阻塞)      │
  └─────────────────────────────────────────────┘
对比2PC3PC
阶段数23
阻塞强阻塞超时可自主决策
单点问题严重缓解
数据一致性可能不一致仍可能不一致(网络分区时)
实际使用MySQL XA几乎不用

面试加分点:3PC 在网络分区时仍有问题——PreCommit 阶段如果部分参与者收到 Abort 而其他参与者超时自动 Commit,就会数据不一致。所以实际系统跳过 3PC,直接用 TCC/Saga。


Q15:TCC 模式是什么?和 2PC 有什么本质区别?

记忆点Try 预留 → Confirm 确认 → Cancel 撤销,业务层面的两阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TCC 三阶段:

以 "转账 A→B 100元" 为例:

Try(预留资源):
  A 账户: 冻结 100 元(available: 900, frozen: 100)
  B 账户: 预增 100 元(available: 1000, pending: 100)

Confirm(确认提交):  ← 所有 Try 成功才执行
  A 账户: 扣除冻结(available: 900, frozen: 0)
  B 账户: 确认增加(available: 1100, pending: 0)

Cancel(撤销回滚):   ← 任一 Try 失败执行
  A 账户: 解冻(available: 1000, frozen: 0)
  B 账户: 撤销预增(available: 1000, pending: 0)

TCC vs 2PC:
  ┌──────────────────────────────────────────┐
  │ 2PC: 数据库层面锁定(DB Lock)            │
  │   → 通用但性能差,长时间持有行锁           │
  │                                          │
  │ TCC: 业务层面冻结(业务字段)              │
  │   → 需要业务改造,但不持 DB 锁,性能好    │
  └──────────────────────────────────────────┘
对比2PCTCC
层面数据库/资源管理器业务层
DB 行锁业务冻结字段
性能低(长锁)高(无 DB 锁)
侵入性低(DB 透明)高(需写3个接口)
回滚自动需业务实现 Cancel
适用单一 DB 跨库跨服务分布式事务

TCC 实现要点

  • 空回滚:Try 没执行但 Cancel 被调用 → Cancel 需检测并直接返回
  • 幂等:Confirm/Cancel 可能重试,必须幂等
  • 悬挂:Cancel 先于 Try 到达 → Try 需检查是否已 Cancel

面试加分点:TCC 框架如 Seata TCC 模式、ByteTCC 解决了空回滚/幂等/悬挂问题。TCC 适合强一致性要求高且需要快速响应的场景(如支付),Saga 适合长事务。


Q16:Saga 模式是什么?和 TCC 有什么区别?

记忆点正向操作链 + 补偿操作链,长事务拆分成多个本地事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Saga 模式:

正常流程(正向操作链):
  T1(创建订单) → T2(扣库存) → T3(扣款) → T4(发物流)
                                              成功 ✓

失败补偿(反向补偿链):
  T1(创建订单) → T2(扣库存) → T3(扣款失败!)
                                   ↓
                    C2(恢复库存) ← C1(取消订单)
                         补偿回滚 ✓

两种执行策略:
  ┌────────────────────────────────────┐
  │ 编排(Choreography):               │
  │ 事件驱动,服务间通过事件通信         │
  │ T1完成 → 发事件 → T2 监听执行      │
  │ 优点:松耦合   缺点:流程难追踪     │
  │                                    │
  │ 协调(Orchestration):              │
  │ 中心协调器控制流程                   │
  │ Coordinator → T1 → T2 → T3        │
  │ 优点:流程清晰  缺点:单点风险      │
  └────────────────────────────────────┘
对比TCCSaga
阶段Try-Confirm-CancelTi-Ci(正向+补偿)
资源隔离Try 阶段冻结无隔离(中间状态可见)
一致性较强最终一致
适用短事务、强一致长事务、跨多服务
侵入性高(3个接口)中(2个接口:正向+补偿)
性能更好(无冻结)

面试加分点:Saga 的隔离性问题——中间状态对外可见,可能出现脏读。解决方案:语义锁(在记录上加状态字段标记”处理中”)、交换律(调整操作顺序使补偿更安全)。Seata 的 Saga 模式通过状态机引擎实现编排式 Saga。


Q17:分布式事务的方案怎么选?

记忆点资金用 TCC,长链路用 Saga,非核心用消息最终一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
分布式事务方案选型:

  一致性要求
  强 ↑
     │  ┌────────────┐
     │  │    2PC     │  理论方案,性能差
     │  │  (XA事务)   │  仅限数据库内部
     │  └────────────┘
     │  ┌────────────┐
     │  │    TCC     │  支付、转账
     │  │  (业务补偿) │  强一致+高性能
     │  └────────────┘
     │  ┌────────────┐
     │  │   Saga     │  订单、物流
     │  │  (补偿链)   │  长事务,最终一致
     │  └────────────┘
     │  ┌────────────┐
     │  │ 消息事务    │  通知、积分
     │  │(最终一致性) │  异步补偿
     │  └────────────┘
  弱 ↓
     └──────────────────────────→ 性能/吞吐
                             高
方案一致性性能适用场景代表框架
2PC/XA强一致单一数据库跨库MySQL XA
TCC强一致资金交易Seata TCC
Saga最终一致长事务/跨多服务Seata Saga
本地消息表最终一致异步通知自研
事务消息最终一致异步通知RocketMQ

面试加分点:Seata 框架提供四种模式——AT(自动补偿,类似2PC但性能更好)、TCC、Saga、XA。AT 模式通过 undo log 实现自动回滚,是互联网公司最常用的方案。


第四部分:分布式协调与时钟

Q18:Gossip 协议是什么?有哪些传播模式?

记忆点像”传八卦”一样传播信息,最终所有人都知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Gossip 传播过程:

  Round 1: Node A 知道消息,随机告诉 B
  ┌───┐    ┌───┐    ┌───┐    ┌───┐
  │ A │──→│ B │    │ C │    │ D │
  │ ★ │    │ ★ │    │   │    │   │
  └───┘    └───┘    └───┘    └───┘

  Round 2: A 和 B 各随机告诉一个
  ┌───┐    ┌───┐    ┌───┐    ┌───┐
  │ A │    │ B │──→│ C │    │ D │
  │ ★ │──→│ ★ │    │ ★ │    │   │
  └───┘  ↓ └───┘    └───┘    └───┘
       ┌───┐
       │ D │ ← A 随机选中 D
       │ ★ │
       └───┘

  Round 3: 所有节点都知道了
  收敛时间 = O(log N)
传播模式原理特点
Anti-Entropy定期随机交换全部数据可靠但带宽大
Rumor-Mongering传播”热门”消息,冷却后停止快但可能遗漏
Push我有新消息,推给你消息多时冗余大
Pull你有什么新消息?节点多时效率高
Push-Pull双向交换收敛最快
使用系统用途
Redis Cluster节点状态/故障检测
Cassandra节点成员/元数据同步
Consul服务发现/健康检查
SWIM成员检测协议

面试加分点:Gossip 的数学特性——N 个节点中传播一条消息,O(log N) 轮后所有节点都能收到。带宽开销是 O(N log N)。适合大规模集群的元数据同步,不适合强一致性数据。


Q19:逻辑时钟、向量时钟是什么?解决什么问题?

记忆点物理时钟不可靠,逻辑时钟定因果,向量时钟判并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Lamport 逻辑时钟:
  规则:
  1. 本地事件 → 时钟 +1
  2. 发送消息 → 附带当前时钟
  3. 收到消息 → max(本地, 消息) + 1

  Node A:  1    2         4    5
           │    │         ↑    │
           │    └────→────┘    │
  Node B:     1    2    3      │
              │         │     ↗
              │         └──→─┘
  Node C:        1    2         6

  局限:a→b(a 因果先于 b),但 LC(a)<LC(b) 不能推出 a→b

向量时钟(Vector Clock):
  每个节点维护一个向量 [A的计数, B的计数, C的计数]

  Node A: [1,0,0] → [2,0,0] → [2,0,0] 发送
  Node B: [0,0,0] → [0,1,0] → [2,2,0] 收到A的消息
  Node C: [0,0,0] → [0,0,1]

  比较规则:
  [2,2,0] vs [0,0,1]
  2>0 且 0<1 → 无法比较 → 并发事件!

  [1,0,0] vs [2,2,0]
  1≤2 且 0≤2 且 0≤0 → [1,0,0] happened-before [2,2,0]
对比Lamport 时钟向量时钟
表示单个整数N 维向量
能力定偏序(a→b 则 LC(a)<LC(b))判因果+判并发
空间O(1)O(N)
使用Raft Term、ZooKeeper ZXIDDynamoDB、Riak

面试加分点:向量时钟的空间问题——N 个节点就要 N 维。DynamoDB 用了变体:只跟踪服务端节点(而非客户端),并有裁剪机制。还有一种替代方案是 Version Vector,语义类似但用于数据副本版本控制。


Q20:etcd 和 ZooKeeper 有什么区别?怎么选?

记忆点etcd 用 Raft + gRPC 现代化,ZK 用 ZAB + Java 历史悠久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
架构对比:

etcd:                          ZooKeeper:
┌──────────────────┐            ┌──────────────────┐
│   gRPC API       │            │  TCP 自定义协议   │
├──────────────────┤            ├──────────────────┤
│   Raft 共识      │            │   ZAB 共识       │
├──────────────────┤            ├──────────────────┤
│   BoltDB 存储    │            │  内存+事务日志    │
│   (B+Tree, MVCC) │            │  (ZNode树)       │
├──────────────────┤            ├──────────────────┤
│   Go 实现        │            │  Java 实现       │
└──────────────────┘            └──────────────────┘

数据模型:
  etcd: 扁平 KV + MVCC 多版本
    /service/node1 → {"addr":"10.0.0.1"}
    /service/node2 → {"addr":"10.0.0.2"}

  ZooKeeper: 层次树形 ZNode
    /
    ├── service
    │   ├── node1 (数据+版本)
    │   └── node2 (数据+版本)
    └── config
        └── db_url
对比etcdZooKeeper
共识算法RaftZAB
APIgRPC + HTTP/JSON自定义 TCP
数据模型扁平 KV树形 ZNode
Watch流式 Watch(不丢事件)单次触发(需重注册)
一致性读线性读(ReadIndex)默认非线性(需 sync)
事务小事务(If-Then-Else)Multi 操作
语言GoJava
容量推荐 < 8GB推荐 < 500MB
典型使用KubernetesKafka、Hadoop、Dubbo

面试加分点:etcd 的 Watch 是基于 MVCC 的流式 Watch,不会丢失事件(ZooKeeper 的 Watch 是单次触发,两次注册之间的事件会丢失)。这也是 Kubernetes 选择 etcd 的重要原因。


Q21:分布式锁有哪些实现方式?各有什么优缺点?

记忆点Redis 快但不安全,ZK/etcd 安全但慢,按场景选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
三种分布式锁实现:

1. Redis 分布式锁:
   SET lock_key unique_id NX PX 30000
   ┌────────┐  SETNX  ┌───────┐
   │Client A│────────→│ Redis │  成功→获得锁
   │        │←────────│       │  失败→未获得锁
   └────────┘         └───────┘
   释放:Lua脚本检查 unique_id 后 DEL

2. ZooKeeper 分布式锁:
   创建临时顺序节点 → 最小序号获得锁
   /lock/
   ├── _lock_0001  ← Client A (最小, 获得锁)
   ├── _lock_0002  ← Client B (Watch 0001)
   └── _lock_0003  ← Client C (Watch 0002)
   释放:删除节点(或会话超时自动删除)

3. etcd 分布式锁:
   基于 Lease + Revision
   创建带 Lease 的 KV → Revision 最小者获得锁
   Watch 前一个 Revision → 等待删除后获得锁
对比RedisZooKeeperetcd
性能最高
可靠性主从切换可能丢锁高(ZAB)高(Raft)
自动释放超时过期会话断开Lease 过期
公平性不公平公平(顺序节点)公平(Revision)
可重入需自己实现需自己实现需自己实现
适用高性能非核心强一致核心K8s 生态

面试加分点:Redis 单机锁的问题——主从切换时锁可能丢失。Martin Kleppmann 指出 RedLock 也不安全(GC 暂停或时钟跳变时)。强一致性场景应该用基于共识算法的 ZK/etcd 锁 + Fencing Token。


第五部分:进阶问题

Q22:什么是 Fencing Token?为什么分布式锁需要它?

记忆点锁过期但进程还活着=危险,Fencing Token 让存储层拒绝旧请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
没有 Fencing Token 的问题:

  Client A 获得锁(过期30s)
  ┌────────┐
  │Client A│──获得锁──→ 开始处理(GC暂停35s...)
  └────────┘                    ↓
                          锁已过期!
  ┌────────┐                    ↓
  │Client B│──获得锁──→ 写数据 X=200
  └────────┘                    ↓
  ┌────────┐                    ↓
  │Client A│ (GC恢复) → 写数据 X=100  ← 覆盖了B的写入!
  └────────┘

有 Fencing Token:

  Client A 获得锁, Token=33
  Client B 获得锁, Token=34 (锁服务递增)

  ┌────────┐
  │Client A│──写入(Token=33)──→ ┌──────────┐
  └────────┘                    │  Storage  │
  ┌────────┐                    │ 已见Token │
  │Client B│──写入(Token=34)──→ │ =34      │
  └────────┘                    │          │
  ┌────────┐                    │ 拒绝!   │
  │Client A│──写入(Token=33)──→ │ 33<34    │ ← 旧Token被拒绝
  └────────┘                    └──────────┘
方案Fencing 支持
ZooKeeper用 ZNode 的 czxid 作为 Token
etcd用 Revision 作为 Token
Redis不原生支持(需自行实现)

面试加分点:Fencing Token 要求存储层(数据库)配合——能够拒绝低于已见 Token 的写入。这也是 Martin Kleppmann 批评 RedLock 的核心论点:如果存储层支持 Fencing Token,那用单机 Redis 锁就够了;如果不支持,RedLock 也不安全。


Q23:什么是 CRDT?如何实现无冲突的多副本并发写入?

记忆点数据结构自带合并规则,不需协调就能保证最终一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CRDT (Conflict-free Replicated Data Type):

传统方式——写冲突需要协调:
  Node A: counter = 5, 写入 counter = 6
  Node B: counter = 5, 写入 counter = 7
  冲突!需要协调谁的值生效

CRDT 方式——数据结构自动合并:
  G-Counter(只增计数器):
  Node A: {A:3, B:2}  → 总计 = 5
  Node B: {A:2, B:4}  → 总计 = 6

  合并:取每个节点的 max
  结果: {A:3, B:4}  → 总计 = 7  ← 自动合并,无冲突!

常见 CRDT 类型:
  ┌──────────────────────────────────────────┐
  │ G-Counter: 只增计数器 {A:n, B:m}         │
  │ PN-Counter: 增减计数器 (P-Counter - N)   │
  │ G-Set: 只增集合 {a, b, c}               │
  │ OR-Set: 可增可删集合(观察-删除)           │
  │ LWW-Register: 最后写入胜(Last-Writer-Wins)│
  │ MV-Register: 多值寄存器(保留所有并发值)    │
  └──────────────────────────────────────────┘
类型合并规则使用场景
G-Counter取 max页面浏览计数
PN-CounterP-N 各取 max点赞/取消赞
LWW-Register时间戳大的胜用户资料更新
OR-Set添加总是胜过删除购物车、协同编辑
使用系统CRDT 类型
Redis CRDB多种 CRDT
RiakMap/Set/Counter/Flag
AutomergeJSON CRDT
Figma协同编辑 CRDT

面试加分点:CRDT 分两种——CmRDT(操作型,传播操作)和 CvRDT(状态型,传播状态+合并函数)。CvRDT 的合并函数必须满足交换律、结合律、幂等性,这在数学上保证了无论消息顺序如何,最终状态一致。


Q24:Raft 的 Learner 节点和 Joint Consensus 成员变更是什么?

记忆点Learner 只复制不投票,Joint Consensus 新旧配置重叠过渡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Learner 节点:
  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Voter A  │  │ Voter B  │  │ Voter C  │  ← 投票节点
  └──────────┘  └──────────┘  └──────────┘
                                    │
                             日志复制│(单向)
                                    ↓
                             ┌──────────┐
                             │ Learner  │  ← 只接收日志,不参与投票
                             │ (新节点) │     不影响 Quorum 计算
                             └──────────┘

  用途:新节点加入时先当 Learner,追上日志后再提升为 Voter
        避免新节点大量落后导致集群不可用

Joint Consensus 成员变更:
  ┌─────────────────────────────────────────────────┐
  │ 直接切换的问题:                                   │
  │ 旧配置 {A,B,C},新配置 {A,B,C,D,E}              │
  │ 旧多数=2,新多数=3                                │
  │ 切换瞬间可能产生两个多数派 → 两个 Leader!         │
  │                                                   │
  │ Joint Consensus 方案:                             │
  │ 阶段1: C_old,new(联合配置)                       │
  │   提案需同时获得旧多数派+新多数派同意               │
  │ 阶段2: C_new(新配置)                             │
  │   切换到新配置                                     │
  │                                                   │
  │ 时间线:                                           │
  │ ──C_old──┤──C_old,new──┤──C_new──→                │
  │          ↑             ↑                           │
  │        提交联合配置   提交新配置                     │
  └─────────────────────────────────────────────────┘
etcd 的做法说明
单步变更每次只加或减一个节点
Learner新节点先当 Learner 追日志
安全性单步变更不需要 Joint Consensus

面试加分点:etcd 采用单步成员变更(每次只增减一个节点),数学上可证明不会产生两个多数派,比 Joint Consensus 实现简单。但如果需要同时变更多个节点,就必须用 Joint Consensus 或分多步执行。


Q25:如何设计一个高可用的分布式配置中心?

记忆点etcd/ZK 做存储 + Watch 推送 + 本地缓存兜底

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
配置中心架构:

  ┌─────────────────────────────────────────────┐
  │              配置管理后台 (Web UI)            │
  │  修改配置 → 校验 → 灰度发布 → 全量发布       │
  └──────────────────┬──────────────────────────┘
                     │ 写入
                     ↓
  ┌─────────────────────────────────────────────┐
  │           配置存储(etcd / ZK 集群)          │
  │  ┌──────┐  ┌──────┐  ┌──────┐              │
  │  │Node 1│──│Node 2│──│Node 3│  Raft 共识    │
  │  └──────┘  └──────┘  └──────┘              │
  └──────────────────┬──────────────────────────┘
                     │ Watch 推送变更
          ┌──────────┼──────────┐
          ↓          ↓          ↓
  ┌──────────┐ ┌──────────┐ ┌──────────┐
  │ App SDK  │ │ App SDK  │ │ App SDK  │
  │┌────────┐│ │┌────────┐│ │┌────────┐│
  ││本地缓存 ││ ││本地缓存 ││ ││本地缓存 ││ ← 配置中心挂了也能用
  │└────────┘│ │└────────┘│ │└────────┘│
  └──────────┘ └──────────┘ └──────────┘

高可用保障:
  1. 存储层:etcd 3/5 节点 Raft 集群
  2. 推送层:Watch 长连接 + 定时拉取兜底
  3. 客户端:本地文件缓存 + 内存缓存
  4. 变更管理:灰度发布 → 观察 → 全量
组件ApolloNacosetcd 直接用
存储MySQL + 缓存MySQL/嵌入式etcd 自身
推送长轮询长连接 + UDP 推送gRPC Watch
灰度需自建
多环境✅ Namespace需自建
权限RBAC
回滚需自建

面试加分点:配置中心的”最后一道防线”是本地文件缓存——即使配置中心集群完全不可用,应用重启时也能从本地文件加载上次的配置,保证可用性。Apollo 和 Nacos 都实现了这个机制。

本文由作者按照 CC BY 4.0 进行授权