分布式理论与一致性协议面试题 —— 从 CAP 定理到 Raft 共识的深度问答
覆盖CAP/BASE理论、Paxos/Multi-Paxos、Raft(选举/日志复制/安全性)、ZAB协议、分布式事务(2PC/3PC/TCC/Saga)、Gossip协议、向量时钟、etcd/ZooKeeper对比,25道高频题附架构图
分布式系统是后端架构的核心难点——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 │ │ 最终一致性 │
│ 持久性 │ │ │
├────────────────────┤ ├────────────────────────┤
│ 强一致 → 高延迟 │ │ 最终一致 → 高可用 │
│ 单机/小规模 │ │ 大规模分布式 │
└────────────────────┘ └────────────────────────┘
| 维度 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致(实时) | 最终一致(延迟) |
| 可用性 | 可能阻塞 | 基本可用 |
| 适用场景 | 银行转账 | 电商库存/社交点赞 |
| 实现复杂度 | 低(数据库保证) | 高(应用层补偿) |
| 性能 | 低(锁等待) | 高(异步) |
面试加分点: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 Paxos | Multi-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-Only | Leader 不删不改已有日志 |
| 状态机安全 | 相同 Index 应用相同命令 |
面试加分点:Raft 论文共识正确性的关键是 Leader Completeness Property。配合成员变更时的 Joint Consensus(联合共识),确保变更过程中不会产生两个 Leader。
Q11:Raft 和 Paxos 有什么区别?为什么说 Raft 更工程化?
记忆点:Paxos 理论优雅但难实现,Raft 可理解性优先
| 对比 | Paxos | Raft |
|---|---|---|
| 设计目标 | 理论完备性 | 可理解性 |
| Leader | Multi-Paxos 才有 | 核心设计 |
| 日志 | 可乱序提交 | 严格顺序提交 |
| 成员变更 | 未明确规定 | Joint Consensus |
| 实现难度 | 极高(论文到工程鸿沟大) | 低(论文即可实现) |
| 学术论文 | 1990 年 Lamport | 2014 年 Stanford |
| 工业实现 | Chubby、Spanner | etcd、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
| 对比 | Raft | ZAB |
|---|---|---|
| 设计目标 | 通用共识 | ZooKeeper 原子广播 |
| 逻辑时钟 | Term | epoch |
| 日志标识 | (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:正式提交 │
│ │
│ 关键改进:参与者超时后可自主提交(减少阻塞) │
└─────────────────────────────────────────────┘
| 对比 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 | 3 |
| 阻塞 | 强阻塞 | 超时可自主决策 |
| 单点问题 | 严重 | 缓解 |
| 数据一致性 | 可能不一致 | 仍可能不一致(网络分区时) |
| 实际使用 | 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 锁,性能好 │
└──────────────────────────────────────────┘
| 对比 | 2PC | TCC |
|---|---|---|
| 层面 | 数据库/资源管理器 | 业务层 |
| 锁 | 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 │
│ 优点:流程清晰 缺点:单点风险 │
└────────────────────────────────────┘
| 对比 | TCC | Saga |
|---|---|---|
| 阶段 | Try-Confirm-Cancel | Ti-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 ZXID | DynamoDB、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
| 对比 | etcd | ZooKeeper |
|---|---|---|
| 共识算法 | Raft | ZAB |
| API | gRPC + HTTP/JSON | 自定义 TCP |
| 数据模型 | 扁平 KV | 树形 ZNode |
| Watch | 流式 Watch(不丢事件) | 单次触发(需重注册) |
| 一致性读 | 线性读(ReadIndex) | 默认非线性(需 sync) |
| 事务 | 小事务(If-Then-Else) | Multi 操作 |
| 语言 | Go | Java |
| 容量 | 推荐 < 8GB | 推荐 < 500MB |
| 典型使用 | Kubernetes | Kafka、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 → 等待删除后获得锁
| 对比 | Redis | ZooKeeper | etcd |
|---|---|---|---|
| 性能 | 最高 | 中 | 中 |
| 可靠性 | 主从切换可能丢锁 | 高(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-Counter | P-N 各取 max | 点赞/取消赞 |
| LWW-Register | 时间戳大的胜 | 用户资料更新 |
| OR-Set | 添加总是胜过删除 | 购物车、协同编辑 |
| 使用系统 | CRDT 类型 |
|---|---|
| Redis CRDB | 多种 CRDT |
| Riak | Map/Set/Counter/Flag |
| Automerge | JSON 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. 变更管理:灰度发布 → 观察 → 全量
| 组件 | Apollo | Nacos | etcd 直接用 |
|---|---|---|---|
| 存储 | MySQL + 缓存 | MySQL/嵌入式 | etcd 自身 |
| 推送 | 长轮询 | 长连接 + UDP 推送 | gRPC Watch |
| 灰度 | ✅ | ✅ | 需自建 |
| 多环境 | ✅ Namespace | ✅ | 需自建 |
| 权限 | ✅ | ✅ | RBAC |
| 回滚 | ✅ | ✅ | 需自建 |
面试加分点:配置中心的”最后一道防线”是本地文件缓存——即使配置中心集群完全不可用,应用重启时也能从本地文件加载上次的配置,保证可用性。Apollo 和 Nacos 都实现了这个机制。