面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| CAP/BASE | 分区发生时取舍、最终收敛 | 不同接口能否有不同选择 |
| 一致性哈希 | 哈希环、顺时针路由、虚拟节点 | 数据倾斜、与哈希槽对比、热点 key 为什么无解 |
| Redis 锁 | 原子加锁、租约、持有者校验 | 暂停、主从切换、续期失败 |
| Redisson | 看门狗续期、Hash 可重入、Lua | 续期就万无一失吗、传 leaseTime 的误用 |
| RedLock | 多数派加锁、时钟假设 | Kleppmann 之争、效率锁 vs 正确性锁 |
| ZooKeeper 锁 | 临时顺序节点、监听前驱 | 会话过期、羊群效应、公平性 |
| fencing token | 单调令牌、下游拒绝旧写 | 下游如何保存和比较令牌 |
| 分布式 ID | 唯一、有序、吞吐、可用 | 时钟回拨、机器号冲突 |
| Raft | 任期、选举、日志复制、多数派 | 网络分区、日志冲突、成员变更 |
| Paxos/ZAB | 多数派共识、主从广播 | 与 Raft 的目标和实现差异 |
| 幂等 | 业务唯一键、状态机、结果复用 | 并发首请求、幂等记录过期 |
| 最终一致性 | 可靠事件、幂等消费、补偿对账 | 消息永久失败、人工兜底 |
| 脑裂 | 多主写入、法定人数、隔离 | 旧主恢复后如何处理 |
| 分布式基础 | 系统定义、分布式 vs 微服务、BASE | 适用场景、引入后的问题与收益 |
| 可用性与故障模型 | 拜占庭故障、时钟不可靠、F+1 容错 | FLP 不可能、活锁与死锁 |
分布式题避免使用“绝对安全”。应明确故障模型、假设条件和极端情况下的业务兜底。
一、分布式基础
什么是分布式系统?
难度 🟢
分布式系统由多台独立节点通过网络协作完成共同任务,对外尽量表现为一个整体。
引入分布式主要为了:
- 水平扩展,突破单机容量上限。
- 多副本容灾,提升可用性。
- 按业务或数据边界独立演进。
同时会引入网络不可靠、部分失败、数据一致性、时钟和运维复杂度等问题。
分布式和微服务有什么区别?
- 分布式:强调多个节点协同,是部署和运行形态。
- 微服务:强调按业务边界拆分,是软件架构风格。
单体应用部署多个副本属于分布式,但不是微服务;微服务通常需要分布式部署。
CAP 定理如何理解?
难度 🟡
发生网络分区时,系统无法同时保证:
- 一致性 C:所有节点读取到同一份最新数据。
- 可用性 A:每个请求都能得到非错误响应。
- 分区容错 P:节点通信中断时系统仍能工作。
分布式系统通常必须容忍分区,因此是在具体操作上选择偏 C 还是偏 A,而不是脱离场景给整个系统贴标签。
BASE 理论是什么?
- Basically Available:基本可用。
- Soft State:允许中间状态。
- Eventually Consistent:最终一致。
BASE 不是放弃一致性,而是允许短时间不一致,通过重试、补偿和对账最终收敛。
一致性哈希为什么需要虚拟节点?
频次 ★★★ · 难度 🟡 · 高频:美团
是什么:普通取模路由 hash(key) % N 的致命伤是 N 一变几乎全部 key 重新映射——缓存集群扩容一台,命中率瞬间归零,等于自己制造缓存雪崩。一致性哈希把哈希空间首尾相接成 0 ~ 2^32-1 的环:节点按 hash(节点标识) 落在环上,key 沿顺时针遇到的第一个节点就是归属。增删节点只影响相邻的一段弧,重映射比例从”几乎 100%“降到约 1/N。
为什么需要虚拟节点:只把 N 个物理节点丢上环有两个问题——
- 数据倾斜:节点少时哈希落点很难均匀,可能某节点独占大半个环
- 级联压垮:节点宕机后,它负责的整段弧全部顺移给下一个节点,下一个节点极可能跟着被打挂
虚拟节点的做法:每个物理节点派生 100~200 个虚拟节点(node-A#1、node-A#2…)散布上环。落点多了靠大数定律趋于均匀;某物理节点下线时,它的众多小段弧被打散分摊给所有存活节点,而不是砸向单个邻居。还有个附带好处:给高配机器多派虚拟节点即可实现按权重分配。
源码(TreeMap 实现,生产思路同 Ketama):
public class ConsistentHashRouter {
// 环:虚拟节点哈希值 → 物理节点。TreeMap 的有序性就是"环上顺时针"
private final TreeMap<Long, String> ring = new TreeMap<>();
private static final int VIRTUAL_NODES = 160;
public void addNode(String node) {
for (int i = 0; i < VIRTUAL_NODES; i++)
ring.put(hash(node + "#" + i), node);
}
public void removeNode(String node) {
for (int i = 0; i < VIRTUAL_NODES; i++)
ring.remove(hash(node + "#" + i));
}
public String route(String key) {
// ceilingEntry = 顺时针第一个 ≥ 落点的虚拟节点
Map.Entry<Long, String> e = ring.ceilingEntry(hash(key));
// 为 null 说明越过 12 点方向,绕回环上第一个节点
return (e != null ? e : ring.firstEntry()).getValue();
}
private long hash(String s) { /* MurmurHash/MD5,别用 String.hashCode(分布差) */ }
}对比哈希槽(Redis Cluster):
| 维度 | 一致性哈希 + 虚拟节点 | 哈希槽(16384 槽) |
|---|---|---|
| key → 节点 | 算法隐式决定(落环顺时针) | 显式映射表(槽 → 节点),可人工指定 |
| 扩缩容 | 拓扑一变自动重路由 | 必须显式迁槽(MIGRATE 原子搬 key) |
| 迁移控制力 | 弱:哪些 key 换家由哈希说了算 | 强:迁哪个槽、迁多少、何时迁都可控 |
| 元数据 | 无(各节点算法一致即可) | 槽位图需在集群内传播(gossip) |
| 典型用户 | Memcached 客户端分片、Nginx hash ... consistent、Dubbo 负载均衡 | Redis Cluster |
本质取舍:一致性哈希用算法省掉元数据,哈希槽用元数据换控制力——数据库类系统搬数据要原子性和节奏控制,所以 Redis Cluster 选显式槽表,见Redis”Cluster 为什么是 16384 个槽”。
常见追问:
- 虚拟节点数越多越好吗?→ 越多越均匀(标准差按 √n 收敛),但环越大、节点增删要操作的条目越多;工程经验 100~200/物理节点(Ketama 用 160)
- 虚拟节点解决倾斜后,还会有热点吗?→ 会。虚拟节点解决的是节点分布倾斜,解决不了单 key 访问热点(明星商品都路由到同一节点),后者要靠本地缓存、key 打散多副本
- 一致性哈希在哪些中间件里真实存在?→ Memcached 客户端、Dubbo
ConsistentHashLoadBalance、Nginx upstream consistent hash、Cassandra/DynamoDB 的分区环
通用概念:虚拟节点是给「逻辑单元 → 物理资源」加一层间接层来同时解决均匀性与迁移粒度——与哈希槽(槽↔节点)、操作系统虚拟内存(页↔物理帧)、Kafka 分区(分区↔broker)同构:加一层映射,分布和归属就成了可调的自由度。
二、分布式锁
Redis 分布式锁如何实现?
难度 🔴
加锁需要原子执行:
SET lock_key unique_value NX PX 10000NX:锁不存在才设置。PX:设置租约,防止客户端异常后永久占锁。unique_value:标识锁持有者,避免误删他人的锁。
解锁必须使用 Lua 脚本原子完成“比较持有者并删除”。
Redis 分布式锁有哪些风险?
- 业务执行超过租约,锁提前过期。
- GC 停顿或网络延迟导致续期不及时。
- 主节点写入后宕机,数据未同步到新主节点,可能出现双持有者。
- 客户端重试、超时和异常路径处理不当。
可通过自动续期、幂等、数据库约束和 fencing token 降低风险。锁不能替代业务正确性约束。
什么是可重入锁和自动续期?
- 可重入:同一线程或客户端可以重复获得同一把锁,需要记录持有者和重入次数。
- 自动续期:持有锁期间定期延长租约,业务完成后停止续期并释放。
续期只能降低锁提前过期概率,不能消除进程停顿、网络分区和主从切换问题。
Redis 锁和 ZooKeeper 锁如何选择?
ZooKeeper 的 Session、Watch、Znode、选主和注册中心细节见 ZooKeeper与注册中心。
| Redis | ZooKeeper | |
|---|---|---|
| 性能 | 高 | 相对较低 |
| 实现 | SET NX PX + Lua | 临时顺序节点 + Watch |
| 排队公平性 | 需要额外实现 | 天然按顺序节点排队 |
| 故障处理 | 依赖租约 | 会话失效后临时节点删除 |
| 适用 | 高吞吐、可容忍极端竞争风险 | 更重视协调语义和顺序性 |
不要为了分布式锁单独引入团队不熟悉的复杂组件。
什么是 fencing token?
锁服务每次成功加锁时返回单调递增令牌,下游只接受令牌更大的写操作。
即使旧客户端因暂停后恢复并继续执行,下游也能根据较小令牌拒绝过期持有者的写入。这比只依赖锁租约更可靠。
Redisson 看门狗(Watchdog)如何实现自动续期?
手写 SET NX PX 的痛点:租约设短了业务没跑完锁就飞了,设长了故障恢复慢。Redisson 的解法:
- 加锁不指定过期时间时,默认租约 30s(
lockWatchdogTimeout),同时启动看门狗定时任务:每隔 租约/3(10s) 检查锁是否仍被当前线程持有,是则把租约重置回 30s - 锁用 Hash 结构存储:
{lockKey: {线程标识: 重入次数}},天然支持可重入;加锁/续期/解锁都是 Lua 脚本保证原子性 unlock()或客户端进程挂掉(定时任务消失)→ 停止续期,锁最多 30s 后自动释放
常见追问
- 看门狗能保证锁绝不提前失效吗?→ 不能。长 GC 停顿超过租约、网络分区期间续期失败,锁照样被别人拿走——这正是第 2 节的锁风险,兜底仍是幂等 + fencing token。
- 显式传了
leaseTime会怎样?→ 看门狗不启动,到期即释放。这是常见误用点:以为有自动续期,实际传参后没有。
RedLock 是什么?为什么有争议?
是什么:Redis 作者 antirez 提出的多实例加锁算法——向 N(如 5)个独立的 Redis 实例依次加锁,在多数派(3 个)成功且总耗时小于锁有效期时视为加锁成功,以消除单实例主从切换丢锁的问题。
争议(Martin Kleppmann 的批评):
- 安全性依赖时钟假设:某实例时钟跳变(NTP 校时、运维改时间)会让锁提前过期,多数派被击穿
- 进程停顿(GC)问题依然存在:拿到锁后停顿,恢复时锁已过期但自己不知道——RedLock 无法解决,仍需 fencing token
- antirez 回应称时钟跳变可运维规避,但学界普遍认为:要正确性就用共识系统(ZooKeeper/etcd),要效率单实例 Redis 足够,RedLock 两头不占
答题要点:能讲清”效率型锁 vs 正确性型锁”的分野即可——防重复计算用单 Redis 锁 + 幂等兜底;钱相关的互斥用 ZooKeeper/etcd 或数据库约束。Redisson 提供的 RedissonRedLock 也已被官方标记废弃。
三、分布式 ID
分布式 ID 需要满足什么条件?
- 全局唯一。
- 高可用、高吞吐。
- 最好趋势递增,减少数据库索引页分裂。
- 可接受的长度和存储成本。
- 必要时支持时间、机房或业务信息。
常见分布式 ID 方案如何选择?
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 本地生成、全局唯一 | 长且无序 |
| 数据库号段 | 趋势递增、性能高 | 依赖发号服务和号段管理 |
| Redis INCR | 简单、递增 | 依赖 Redis 可用性和持久化 |
| 雪花算法 | 本地生成、趋势递增 | 时钟回拨和机器 ID 管理 |
雪花算法如何处理时钟回拨?
常见策略:
- 小幅回拨时等待时间追上。
- 切换备用机器 ID 或时间序列。
- 使用逻辑时钟。
- 大幅回拨时拒绝服务并告警。
必须同时保证机器 ID 不重复,否则不同实例可能生成相同 ID。
四、共识算法
Raft 算法解决什么问题?
难度 🔴
Raft 用于让多个节点对一组有序日志达成共识,核心包括:
- Leader 选举。
- 日志复制。
- 安全性约束。
客户端写入 Leader,日志复制到多数节点后提交,再应用到状态机。任期号和日志完整性规则保证已提交日志不会被新 Leader 覆盖。
Raft 为什么需要多数派?
任意两个多数派集合一定有交集,因此已经被多数节点确认的信息不会在下一次多数派选举中完全丢失。
三节点可容忍一个节点故障,五节点可容忍两个节点故障。增加偶数节点通常不会提升可容忍故障数。
通用概念:任意两个多数派必定相交,所以新决策者一定见过旧决策,脑裂在数学上被排除;同一支点撑起 ZAB、哨兵选主、RedLock。
Raft 和 Paxos 的区别?
- Paxos 是经典共识理论基础,角色和流程更抽象。
- Raft 将问题拆为选举、日志复制和成员变更,更容易理解和工程实现。
- 两者都依赖多数派交集保证安全性。
ZAB 协议是什么?
ZAB 是 ZooKeeper 的原子广播协议:
- 正常阶段由 Leader 对事务排序并广播。
- 多数 Follower 确认后提交。
- Leader 故障时进入崩溃恢复,选出拥有合适事务历史的新 Leader。
ZAB 更关注主备系统中的有序事务广播,Raft 更强调可理解的复制状态机共识。
五、幂等与一致性
什么是幂等?如何实现?
难度 🔴
同一个请求执行一次或多次,对业务产生的最终效果相同。
常见方案:
- 数据库唯一约束。
- 业务状态机条件更新。
- 幂等 Key + 处理结果表。
- Token 一次性消费。
- MQ 消费记录。
查询天然幂等,但创建、扣款、发券等写操作必须显式设计。
通用概念:重试、重投、重复点击制造重复的方式不同,应对只能是同一种——见幂等:超时不等于失败,你无法阻止重复,只能让重复无害。
如何设计幂等 Key?
幂等 Key 应稳定代表同一次业务意图,例如:
业务类型 + 用户/订单 + 请求序号不能每次重试都生成新 UUID,否则服务端无法识别重复请求。还要考虑保存期限、并发竞争和结果复用。
最终一致性如何保证?
典型闭环:
本地事务记录业务数据和待发送事件
↓
可靠投递消息
↓
消费者幂等处理
↓
失败重试、死信、补偿
↓
定时对账和人工兜底“最终”必须有可观测的时限和补偿机制,不能只依赖无限重试。
六、可用性与故障模型
什么是脑裂?
网络分区后,多个节点都认为自己是主节点并接受写入,导致数据分叉。
常见治理:
- 多数派选举。
- 租约或 fencing token。
- STONITH 等隔离旧主节点。
- 写入前确认法定人数。
什么是拜占庭故障?
节点不仅可能宕机,还可能返回错误、矛盾或恶意信息。普通业务集群通常假设节点是崩溃故障模型;区块链等不可信环境才常讨论拜占庭容错。
分布式系统为什么不能依赖本机时间?
不同机器时钟可能漂移或回拨,网络也会导致事件观察顺序不同。
因此:
- 过期判断应容忍时钟误差。
- 业务顺序优先使用版本号、日志序号或数据库序列。
- 不能仅凭两个服务的日志时间判断严格因果关系。