Appearance
分布式理论
系统设计面试的起点:先理解分布式的基本难题,才能谈方案。
Q1: 什么是 CAP 定理? 「🟢 校招/初级」
考察点:分布式系统的核心约束。
参考答案:
- CAP:一个分布式系统最多同时满足 一致性(C)、可用性(A)、分区容错性(P) 中的两个。
- P 是必须的(网络分区不可避免),所以实际是 CP 或 AP 的取舍:
- CP:保证一致性,分区时部分请求不可用。代表:ZooKeeper、Etcd、Redis Cluster。
- AP:保证可用性,允许数据暂时不一致。代表:Cassandra、Eureka、DNS。
- 注意:CAP 的一致性指线性一致性(强一致),不是最终一致。
追问延伸:
- BASE 理论和 CAP 的关系?
- 为什么 ZooKeeper 选 CP 而不是 AP?
Q2: 什么是 BASE 理论? 「🟢 校招/初级」
考察点:对 CAP 中 AP 方向的工程化实践。
参考答案:
- BAsically Available:基本可用,允许损失部分可用性(如响应时间变长、降级页面)。
- Soft State:软状态,允许数据存在中间状态(如订单"支付中")。
- Eventually Consistent:最终一致性,数据在一段时间后达到一致。
- 本质:用最终一致性换取高可用,是大多数互联网系统的选择。
追问延伸:
- 最终一致性的延迟一般怎么控制?
- 强一致和最终一致的业务场景怎么区分?
Q3: 分布式一致性算法有哪些? 「🟡 中级」
考察点:共识算法的理解。
参考答案:
- Paxos:理论完备但实现复杂,是多数算法的理论基础。
- Raft:Paxos 的简化版,易理解易实现;Leader 选举 + 日志复制 + 安全性保证。代表:Etcd、Consul。
- ZAB:ZooKeeper 的协议,类似 Raft 但针对 ZK 场景优化。
- 选型:新系统优先 Raft 系(Etcd/Consul);ZooKeeper 生态成熟但运维较重。
追问延伸:
- Raft 的 Leader 选举过程?
- 脑裂(Split Brain)是什么?怎么防止?
Q4: 什么是分布式事务?有哪些方案? 「🟡 中级」
考察点:跨服务/跨库数据一致性的核心难题。
参考答案:
- 2PC(两阶段提交):协调者询问参与者是否可提交 → 全部 OK 则提交;缺点:同步阻塞、协调者单点。
- 3PC:引入超时和预备阶段,降低阻塞,但仍不完美。
- TCC:Try-Confirm-Cancel,业务层面实现补偿,灵活但开发成本高。
- Saga:长事务拆成多个本地事务 + 补偿操作,正向执行失败则反向补偿。
- 本地消息表 / 事务消息:最终一致性方案,如 RocketMQ 事务消息、Canal 订阅 binlog。
追问延伸:
- 互联网公司最常用的方案是什么?(最终一致性 + 补偿)
- Seata 支持哪几种模式?
Q5: 什么是幂等性?分布式系统为什么需要幂等? 「🟡 中级」
考察点:分布式系统的基本设计原则。
参考答案:
- 幂等:同一操作执行一次和执行多次结果相同。
- 为什么需要:网络超时、重试、消息重复消费都可能导致操作被多次执行。
- 实现手段:唯一请求 ID + 去重表、数据库唯一约束、乐观锁(version 字段)、状态机(只允许单向流转)。
追问延伸:
- 支付回调怎么保证幂等?
- 幂等和"只执行一次"有什么区别?
Q6: Raft 协议的 Leader 选举和日志复制是怎样的? 「🔴 高级」
考察点:Raft 共识算法的核心机制。 参考答案:
Raft 三大核心机制:
Leader 选举:
- 节点角色:Leader(处理请求)、Follower(被动接收)、Candidate(竞选者)
- 选举超时(150-300ms 随机):Follower 超时未收到 Leader 心跳 → 变为 Candidate
- Candidate 增加任期号(term),发起投票请求(RequestVote RPC)
- 收到多数节点投票 → 成为 Leader
- Leader 定期发心跳(AppendEntries RPC)维持地位
- 防止脑裂:每个 term 每个节点只投一票(先到先得)
日志复制:
- Leader 收到客户端请求 → 写入本地日志(uncommitted)
- Leader 通过 AppendEntries RPC 把日志发给所有 Follower
- 多数 Follower 确认后 → Leader 标记为 committed → 返回客户端
- Leader 在下次心跳中通知 Follower 也 commit
- 保证:已 commit 的日志不会丢失、所有节点状态一致
安全性保证:
- 选举限制:Candidate 的日志必须至少和其他多数节点一样新(不能选旧日志的节点)
- 提交规则:Leader 只提交当前 term 的日志(不间接提交旧 term 日志)
Raft vs Paxos:
- Raft 更易理解(强 Leader 模型、日志是连续的)
- Paxos 更通用(允许多 Leader、日志可乱序)
- 工业界多用 Raft(Etcd、Consul、TiKV、CockroachDB)
Raft 应用:
- Etcd(Kubernetes 的配置中心)
- Consul(服务发现)
- TiKV(TiDB 存储引擎)
- Nacos(Raft + Distro 混合)
追问延伸:
- 网络分区时 Raft 会怎样?(少数派分区不能选出 Leader,多数派继续工作)
- Raft 怎么处理日志不一致?(Leader 强制 Follower 复制自己的日志)
Q7: Paxos 协议的核心思想是什么?和 Raft 有什么区别? 「🔴 高级」
考察点:分布式共识算法的理论基础。 参考答案:
Paxos 核心思想:
- 通过多数派(Quorum)达成一致,容忍少数节点故障
- 三个角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者)
Basic Paxos 流程:
- Prepare 阶段:
- Proposer 生成全局唯一递增的编号 N,发送 Prepare(N) 给所有 Acceptor
- Acceptor 收到 Prepare(N):
- 如果 N > 已承诺的最大编号 → 承诺不再接受 < N 的提议,返回之前已接受的提议
- 否则 → 拒绝
- Accept 阶段:
- Proposer 收到多数 Acceptor 的承诺 → 发送 Accept(N, value)
- value = 收到的已接受提议中编号最大的值(如果没有则用自己的值)
- Acceptor 收到 Accept(N, value):
- 如果 N >= 已承诺的最大编号 → 接受,记录提议
- 否则 → 拒绝
- Learn 阶段:
- Acceptor 接受后通知 Learner
Multi-Paxos(实际使用):
- Basic Paxos 每次达成一致需要两轮 RPC,效率低
- Multi-Paxos:选出一个稳定的 Leader,Leader 直接发 Accept(省去 Prepare)
- Leader 失效后重新选举
- 但 Multi-Paxos 没有具体实现规范(Raft 是 Multi-Paxos 的具体实现)
Paxos vs Raft 对比:
| 维度 | Paxos | Raft |
|---|---|---|
| 理解难度 | 难 | 易 |
| Leader | 可有可无 | 强 Leader |
| 日志 | 可乱序 | 连续有序 |
| 工程实现 | 少(Chubby) | 多(Etcd/TiKV) |
| 理论完备 | 最完备 | 完备但简化 |
追问延伸:
- 为什么 Paxos 难以实现?(协议描述抽象,很多细节未定义)
- Google Chubby 用的是 Paxos 还是 Raft?(Paxos)
Q8: ZAB 协议是什么?和 Raft 有什么区别? 「🔴 高级」
考察点:ZooKeeper 共识协议的理解。 参考答案:
ZAB(ZooKeeper Atomic Broadcast):ZooKeeper 的核心协议,保证主备数据一致性。
ZAB 的两个阶段:
崩溃恢复(Leader 选举):
- 节点状态:LOOKING(选举中)、FOLLOWING、LEADING、OBSERVING
- 选举流程:
- 每个节点投自己(myid + zxid)
- 收到其他节点的投票,比较(zxid 大的优先,zxid 相同则 myid 大的优先)
- 收到多数投票 → 成为 Leader
- 和 Raft 的区别:Raft 用 term(任期),ZAB 用 zxid(事务ID,含 epoch)
消息广播(原子广播):
- Leader 收到写请求 → 分配全局递增的 zxid → 生成 Proposal
- Leader 发送 Proposal 给所有 Follower(类似 Raft 的 AppendEntries)
- Follower 收到 Proposal → 写入本地队列 → 回复 ACK
- Leader 收到多数 ACK → 标记 committed → 发送 COMMIT 给 Follower
- 和 2PC 的区别:只要多数 ACK 就提交,不需要全部
zxid 结构:
- 64 位 = 32 位 epoch + 32 位 counter
- epoch:Leader 周期(每次换 Leader 递增)
- counter:该 epoch 内事务序号
ZAB vs Raft 对比:
| 维度 | ZAB | Raft |
|---|---|---|
| 应用 | ZooKeeper | Etcd/Consul/TiKV |
| 数据模型 | 树形(znode) | KV |
| 日志 | 只同步新增 | 全量同步 |
| Leader选举 | zxid 优先 | term + 日志完整性 |
| 读一致性 | 默认读 Leader(强一致) | 默认线性一致 |
追问延伸:
- ZooKeeper 为什么选 ZAB 不用 Raft?(历史原因,ZK 2008 年发布时 Raft 还没出现)
- ZAB 的 zxid 和 Raft 的 term 有什么区别?
Q9: 如果让你设计一个 RPC 框架,怎么设计? 「🔴 高级」
考察点:系统设计能力 + 对 RPC 原理的理解。 参考答案:
RPC 框架的核心组件:
客户端(Consumer):
- 代理层(Proxy):生成接口的动态代理,让调用远程方法像调用本地方法
- 负载均衡:从注册中心拿到多个 Provider 地址,选择一个(轮询/随机/一致性哈希/最少连接)
- 集群容错:失败重试、失败快速失败、失败自动切换、并行调用
- 序列化:把方法参数序列化为二进制(JSON/Protobuf/Hessian/Kryo)
网络通信层:
- 通信协议:自定义协议(魔数 + 版本 + 序列化类型 + 消息ID + 数据长度 + 数据体)
- IO 模型:Netty(NIO + epoll)、长连接复用
- 编解码:序列化/反序列化 + 拆包粘包处理
服务端(Provider):
- 请求分发:根据接口名+方法名找到对应的实现类和方法
- 反射调用:通过反射执行目标方法
- 线程池:业务线程池处理请求(和 IO 线程隔离)
注册中心:
- 服务注册:Provider 启动时注册(接口→地址列表)
- 服务发现:Consumer 启动时订阅,缓存到本地
- 健康检查:心跳检测,下线不健康节点
- 选型:Nacos/ZooKeeper/Eureka/Consul
治理能力:
- 熔断降级:下游不可用时快速失败 + 降级逻辑
- 限流:控制请求速率
- 链路追踪:TraceId 贯穿全链路
- 监控:QPS、RT、错误率
- 配置中心:动态调整参数
设计要点:
- 长连接 + 多路复用(单个 TCP 连接发多个请求,用 RequestID 区分响应)
- 异步化(CompletableFuture / 回调,不阻塞 IO 线程)
- 超时控制(避免无限等待)
- 优雅上下线(Provider 下线前先通知 Consumer)
主流 RPC 框架:Dubbo(Java)、gRPC(跨语言)、Motan(Java)、Thrift(跨语言)
追问延伸:
- gRPC 和 Dubbo 的区别?
- 为什么 gRPC 用 HTTP/2 而不用自定义 TCP 协议?
Q10: 一致性哈希是什么?解决了什么问题? 「🟡 中级」
考察点:分布式数据分布的经典方案。 参考答案:
传统取模方案的问题:
hash(key) % N,N 是节点数- 节点增减时,N 变化 → 几乎所有 key 的映射都变了 → 大量数据迁移
一致性哈希:
- 把整个哈希空间组织成环(0 ~ 2^32-1)
- 节点映射到环上:
hash(节点IP) → 环上位置 - 数据映射到环上:
hash(key) → 环上位置 → 顺时针找到第一个节点 - 节点增减时只影响相邻段的数据,不全局迁移
虚拟节点(解决数据倾斜):
- 实际节点少时,数据分布不均匀
- 每个物理节点映射多个虚拟节点(如 150-200 个)到环上
- 虚拟节点越多,分布越均匀
应用:
- Redis Cluster(用 16384 槽,类似思想但不同实现)
- Memcached 客户端分片
- Dubbo 负载均衡
- Cassandra 数据分布
Redis Cluster 的方案(不是一致性哈希):
- 固定 16384 个槽,每个节点负责一部分
CRC16(key) % 16384确定槽- 扩容时迁移槽,不影响其他槽的数据
- 比一致性哈希更好管理(槽是有限的,迁移是可控的)
追问延伸:
- 一致性哈希和 Redis 槽位的区别?
- 虚拟节点越多越好吗?(越多越均匀,但内存和查找开销也增大)
Q11: 对外提供一个 API 服务,客户说请求超时了,怎么排查? 「🟡 中级」
考察点:线上问题排查的综合能力。 参考答案:
排查链路(从外到内):
确认问题:
- 是偶发还是必现?超时多久?哪个接口?哪个客户?
- 客户端网络是否正常?是否最近上线了变更?
网络链路:
- DNS 解析是否正常(
dig/nslookup) - 网络连通性(
ping/traceroute) - 防火墙/安全组是否放通
- CDN/WAF 是否有延迟
- 客户端到服务端的网络 RTT
- DNS 解析是否正常(
网关层:
- Nginx/网关日志:看请求是否到达、响应时间、上游状态
- 网关限流是否触发(429)
- 网关超时配置是否合理(proxy_read_timeout)
应用层:
- APM 工具(SkyWalking/Zipkin):看请求在哪个环节慢
- 应用日志:看请求处理时间、有无异常
- 是否有慢 SQL、慢 RPC 调用、锁等待
- GC 是否频繁(
jstat -gc)
下游依赖:
- 数据库:慢查询、锁等待、连接池满
- 缓存:Redis 慢查询、大 Key、热 Key
- 消息队列:消费积压
- 第三方 API:外部服务超时
系统资源:
- CPU/内存/负载是否正常
- 网络带宽是否打满
- 磁盘 IO 是否瓶颈
- 文件句柄/线程数是否打满
定位根因:
- 全链路追踪(TraceId 贯穿所有服务)
- 对比正常时的耗时分布,找出异常环节
常见原因排序:
- 慢 SQL(最常见,索引缺失、全表扫描)
- 下游 RPC 超时(级联超时)
- GC 停顿(Full GC / STW)
- 锁竞争(synchronized / 数据库行锁)
- 网络问题(跨机房、DNS、CDN)
- 资源耗尽(连接池满、线程池满)
追问延伸:
- 怎么设置合理的超时时间?(连接超时 vs 读超时,RT 的 P99 × 2-3 倍)
- 超时后怎么处理?(重试、降级、熔断)
Q12: 分布式锁怎么实现?有哪些方案? 「🔴 高级」
考察点:分布式锁的实现原理和选型能力。 参考答案:
三种主流方案:
1. Redis 实现(最常用):
bash
# 方案一:SETNX + 过期时间(简单但有风险)
SETNX lock:order:123 "value" # 加锁
EXPIRE lock:order:123 30 # 设过期,防死锁
# 问题:SETNX 和 EXPIRE 不是原子操作
# 方案二:SET + NX + PX(推荐,原子操作)
SET lock:order:123 "uuid" NX PX 30000 # 加锁 + 过期
# 释放锁:Lua 脚本保证"判断+删除"原子性释放锁的 Lua 脚本:
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endRedis 锁的三个问题:
- 锁过期但业务没执行完:看门狗(Watchdog)自动续期(Redisson 的
expireRenewal) - 锁被别人释放:value 设为唯一 UUID,释放时 Lua 脚本判断后再删
- 主从切换丢锁:Redis 主节点加锁后宕机,从节点没同步。Redlock 算法(向多个独立 Redis 实例加锁,多数成功才算成功)
2. ZooKeeper 实现(最可靠):
/lock/order/123 → 临时顺序节点- 加锁:创建临时顺序节点,序号最小的获得锁
- 解锁:客户端断开,临时节点自动删除(避免死锁)
- 羊群效应优化:每个节点只监听前一个节点的删除事件
ZK 锁的特点:
- 临时节点 → 客户端断开自动释放(不怕死锁)
- 顺序节点 → 天然公平锁,FIFO
- Watch 机制 → 主动通知,不需要轮询
- CP 模型 → 强一致性,但写入性能不如 Redis
3. Etcd 实现(K8s 生态):
- 加锁:
etcdctl lock order:123或 Lease + Txn - 特点:Raft 强一致、Lease 租约防死锁、版本号实现公平锁
方案对比:
| 维度 | Redis | ZooKeeper | Etcd |
|---|---|---|---|
| 一致性 | AP(主从异步) | CP(ZAB) | CP(Raft) |
| 性能 | 最高 | 中 | 中 |
| 可靠性 | 中(主从丢锁) | 高 | 高 |
| 复杂度 | 低 | 中 | 中 |
| 适用 | 高并发、容忍极小概率丢锁 | 强一致场景 | K8s 生态 |
生产建议:
- 大多数场景用 Redisson(自带看门狗续期、可重入锁、公平锁)
- 金融/强一致场景用 ZooKeeper 或 Etcd
- 不要自己实现分布式锁,用成熟框架
追问延伸:
- Redisson 的看门狗原理是什么?(定时任务每 1/3 过期时间续期)
- Redlock 的争议在哪里?(Martin Kleppmann 和 antirez 的争论)
Q13: 分布式 ID 生成方案有哪些? 「🟡 中级」
考察点:全局唯一 ID 的生成策略。 参考答案:
核心要求:全局唯一、趋势递增、高性能、高可用。
1. UUID:
- 优点:本地生成、无网络开销
- 缺点:无序(B+树插入碎片)、太长(36 字符)、不可读
- 适用:日志追踪 ID、TraceId,不建议做主键
2. 数据库自增:
sql
-- 单机自增(简单但性能瓶颈)
CREATE TABLE id_generator (
id BIGINT AUTO_INCREMENT PRIMARY KEY
);
-- 号段模式(批量取号,减少 DB 访问)
-- 每次取 [start, start+step) 范围的号,用完再取- 优点:简单、有序
- 缺点:单点瓶颈、扩展困难
3. Snowflake(雪花算法)(最常用):
1位符号位 | 41位时间戳(ms) | 10位机器ID | 12位序列号
0 | 1711234567890 | 001 | 000000000001- 41 位时间戳:支持约 69 年(2^41 ms / 1000 / 3600 / 24 / 365)
- 10 位机器 ID:1024 台机器(可拆 5 位数据中心 + 5 位机器)
- 12 位序列号:同一毫内 4096 个 ID
- 优点:有序、高性能、无依赖
- 缺点:依赖时钟(时钟回拨导致重复 ID)
- 时钟回拨解决:等待、报错、用历史时间偏移量
4. 数据库号段模式(美团 Leaf-segment):
+----------+----------+----------+----------+
| biz_tag | max_id | step | desc |
+----------+----------+----------+----------+
| order | 10000 | 2000 | 订单ID |
| payment | 50000 | 2000 | 支付ID |
+----------+----------+----------+----------+- 批量取号:应用启动取 [10000, 12000) 缓存到内存,用完再取
- 双 buffer 优化:当前 buffer 用到 10% 时异步加载下一个,无阻塞
- 优点:有序、高可用、DB 压力小
- 缺点:DB 宕机会短暂影响
5. Redis 自增:
bash
INCR order:id # 原子自增
INCRBY order:id 1000 # 批量取号- 优点:简单、快
- 缺点:持久化问题(RDB 可能丢号)、依赖 Redis
方案对比:
| 方案 | 有序性 | 性能 | 可靠性 | 适用场景 |
|---|---|---|---|---|
| UUID | 差 | 高 | 高 | 日志ID、TraceId |
| 数据库自增 | 强 | 低 | 中 | 小规模 |
| Snowflake | 趋势递增 | 极高 | 高 | 大规模、分布式 |
| 号段模式 | 趋势递增 | 高 | 高 | 中大规模 |
| Redis | 强 | 高 | 中 | 简单场景 |
追问延伸:
- Snowflake 时钟回拨怎么处理?(上次时间戳记内存,回拨报错或等待)
- 百度 UidGenerator 和 Snowflake 的区别?(RingBuffer 缓存预生成)
Q14: Gossip 协议是什么?用在哪些场景? 「🔴 高级」
考察点:分布式传播算法的理解。 参考答案:
Gossip(流言协议):一种最终一致性传播协议,模拟流行病传播。
核心机制:
- 每个节点周期性地随机选几个节点,把数据传给它们
- 收到数据的节点再传给其他随机节点
- 经过 O(log N) 轮后,数据传播到全网
三种传播模式:
- Push:有数据的节点主动告诉别人(快速但后期浪费带宽)
- Pull:没数据的节点主动问别人(后期效率高)
- Push-Pull:双向交互,收敛最快(最常用)
# 一轮传播(Push 模式)
节点 A 有新数据 → 随机选 B、C → 发送给 B、C
B 收到后 → 下一轮随机选 D、E → 发送给 D、E
...
经过 log2(N) 轮 → 全网收到特点:
- 去中心化:没有 Leader,所有节点平等
- 最终一致:保证最终所有节点都收到,但有时间延迟
- 容错性极强:节点宕机不影响传播(其他节点会传)
- 可扩展:O(log N) 轮传播,节点数增多影响不大
- 带宽消耗:后期大量冗余传播(已有数据的节点重复接收)
应用场景:
| 应用 | 用途 |
|---|---|
| Redis Cluster | 节点间传播槽位信息和故障转移 |
| Cassandra | 节点状态同步(存活、负载、schema) |
| Consul | 成员管理和健康检查 |
| Bitcoin | 区块和交易传播 |
Redis Cluster 中的 Gossip:
- 每个节点每秒和 5 个随机节点通信(PING/PONG)
- 传播内容:自身状态、已知节点列表、槽位分配
- 故障检测:节点 A 标记 B 为 PFAIL(疑似下线)→ 通过 Gossip 传播 → 多数节点标记 B 为 PFAIL → B 标记为 FAIL(确定下线)
追问延伸:
- Gossip 传播需要多久?(和节点数的关系:约 log2(N) 轮)
- Gossip 和 Raft 的区别?(Gossip 最终一致、去中心化;Raft 强一致、有 Leader)
Q15: 服务注册中心怎么选?Nacos、Eureka、ZooKeeper、Consul 有什么区别? 「🟡 中级」
考察点:服务发现方案选型能力。 参考答案:
四个主流注册中心对比:
| 维度 | Nacos | Eureka | ZooKeeper | Consul |
|---|---|---|---|---|
| CAP 模型 | AP + CP | AP | CP | CP |
| 一致性协议 | Raft + Distro | 无(P2P 复制) | ZAB | Raft |
| 健康检查 | TCP/HTTP/心跳 | 心跳 | Session/临时节点 | TCP/HTTP/Script |
| 多数据中心 | 支持(集群) | 不支持 | 不支持 | 原生支持 |
| 配置中心 | 一体化 | 无 | 无 | KV 存储 |
| 使用语言 | Java | Java | Java | Go |
| 维护状态 | 活跃 | 停止维护(EOL) | 活跃 | 活跃 |
详细分析:
1. Nacos(阿里开源,推荐):
- 同时支持 AP 和 CP(默认 AP)
- AP 模式:Distro 协议,节点间最终一致,服务发现可用性优先
- CP 模式:Raft 协议,用于配置管理(强一致)
- 优势:注册中心 + 配置中心一体化,国内生态好
- 适用:Spring Cloud Alibaba 体系
2. Eureka(Netflix,已停止维护):
- AP 模型,P2P 复制(非强一致)
- 节点平等,写一个节点后异步复制到其他节点
- 自我保护机制:15 分钟内心跳失败超过 85% → 不再剔除实例(防网络分区误判)
- 问题:停止维护,不推荐新项目使用
3. ZooKeeper:
- CP 模型,ZAB 协议保证强一致
- 临时节点实现服务注册,Session 超时自动注销
- 问题:写入要过半,性能不如 AP 模型;不适合大规模服务发现
- 适用:强一致场景(分布式锁、Leader 选举、元数据管理)
4. Consul(HashiCorp):
- CP 模型,Raft 协议
- 支持 TTL/Script 健康检查(灵活但消耗资源)
- 原生多数据中心支持(WAN Gossip)
- 自带 KV 存储,可作为配置中心
- 适用:多数据中心、Go 生态
AP vs CP 对服务发现的影响:
- AP(Nacos/Eureka):网络分区时,旧实例列表可能残留(调用到已下线服务)→ 重试和熔断兜底
- CP(ZooKeeper/Consul):网络分区时,少数派不可用(查不到服务)→ 牺牲可用性
生产实践:
- 服务发现用 AP(容忍数据少量不一致,但要保证可用)
- 配置管理用 CP(配置必须强一致,不能错)
- Nacos 天然满足这个组合
追问延伸:
- Eureka 自我保护机制是什么?(心跳失败率超 85% 不剔除实例)
- Nacos 怎么同时支持 AP 和 CP?(AP 用 Distro,CP 用 Raft)
Q16: 熔断、降级、限流分别是什么?怎么实现? 「🟡 中级」
考察点:微服务容错三板斧。 参考答案:
三者定位不同:
- 限流:控制请求量,保护系统不被流量压垮(主动防御)
- 熔断:下游不可用时断开调用,快速失败(被动防御)
- 降级:系统压力大时牺牲非核心功能,保证核心链路(有损服务)
1. 限流算法:
计数器(固定窗口):
java
// 每秒最多 100 个请求
if (current_count < 100) {
current_count++;
process();
} else {
reject();
}- 问题:窗口边界突刺(59 秒和 1 秒各 100 个请求 → 200 个/2 秒)
滑动窗口:
- 把时间窗口分成多个小格(如 1 秒分成 10 个 100ms 格子)
- 每格独立计数,总请求数 = 所有格子之和
- 比固定窗口平滑
漏桶算法(Leaky Bucket):
请求 ──→ [漏桶] ───→ 处理(匀速)
↑ 满了丢弃- 请求先入桶,以固定速率出桶
- 平滑输出,但突发请求被限制
令牌桶算法(Token Bucket,推荐):
令牌 ──→ [令牌桶] ──→ 取令牌 ──→ 处理
↑ 固定速率放入 无令牌则拒绝- 以固定速率往桶里放令牌,请求处理需取令牌
- 桶有上限,满了丢弃令牌
- 允许突发(桶里积攒的令牌可瞬间消费)
- 实现:Guava RateLimiter、Sentinel、Redis + Lua
2. 熔断器模式(Circuit Breaker):
┌─── Closed(关闭)──→ 错误率>阈值 ──→ Open(打开)
│ │
│ 超时时间到 │
└─── Closed ←─ 半开(Half-Open) ←─────────────┘
│
探测成功 → Closed
探测失败 → Open- Closed:正常调用,统计错误率
- Open:熔断,直接返回降级响应(fast fail)
- Half-Open:放少量请求探测下游是否恢复
主流实现:
- Sentinel(阿里):限流 + 熔断 + 降级,规则可热更新
- Hystrix(Netflix):熔断 + 降级,已停止维护
- Resilience4j(Hystrix 替代):轻量、函数式风格
- Sentinel vs Hystrix:Sentinel 限流更强;Hystrix 隔离机制更成熟
3. 降级策略:
- 超时降级:请求超时 → 返回默认值 / 缓存
- 异常降级:调用异常 → 返回兜底数据
- 限流降级:达到限流阈值 → 排队 / 拒绝 / 降级响应
- 手动降级:运营通过配置中心手动关闭非核心功能
降级示例:
java
@SentinelResource(value = "queryOrder",
fallback = "queryOrderFallback") // 降级方法
public Order queryOrder(String orderId) {
return orderService.query(orderId);
}
// 降级方法
public Order queryOrderFallback(String orderId) {
return new Order(orderId, "默认商品", 0);
}追问延伸:
- 限流放在哪一层?(网关层做粗粒度限流,应用层做细粒度限流)
- 熔断恢复后怎么防止雪崩?(Half-Open 慢放量,逐步恢复)
Q17: 分布式系统怎么解决时间问题?逻辑时钟和向量时钟是什么? 「🔴 高级」
考察点:分布式系统因果序的理解。 参考答案:
问题背景:
- 分布式系统中没有全局物理时钟(每台机器时间不同步)
- 即使 NTP 同步也有毫秒级误差
- 无法用物理时间判断事件的先后顺序
1. Lamport 逻辑时钟(Leslie Lamport, 1978):
核心思想:用逻辑递增的编号代替物理时间,保证偏序关系。
规则:
- 每个进程维护一个逻辑计数器 C
- 进程内每执行一个事件,C = C + 1
- 发送消息时,C = C + 1,消息带上 C
- 接收消息时,C = max(C_local, C_message) + 1
进程A: A1(C=1) → A2(C=2) → 发送消息(C=3) ──→ ...
进程B: B1(C=1) → 接收消息(C=max(1,3)+1=4) → B2(C=5) → ...- 保证:如果 A 事件因果先于 B 事件,则 C(A) < C(B)
- 逆否命题:C(A) < C(B) 不能推导出 A 因果先于 B(只保证偏序,不保证全序)
- 局限:C(A) < C(B) 无法判断是因果关系还是并发关系
2. 向量时钟(Vector Clock,Mattern/Fidge, 1988):
解决逻辑时钟无法判断并发关系的问题。
规则:
- 每个进程维护一个向量 V[N](N 为进程总数)
- 进程 i 执行事件:V[i][i]++
- 发送消息:带上整个向量 V[i]
- 进程 j 接收消息:V[j][k] = max(V[j][k], V_msg[k]) for all k,然后 V[j][j]++
V(A) = [2, 0, 0] V(B) = [2, 3, 0] V(C) = [2, 0, 1]
判断关系:
- V(A) < V(B):A 的每个分量 <= B,且至少一个 < → A 因果先于 B
- V(A) 和 V(C) 不可比较 → A 和 C 是并发的向量时钟比较:
- 因果序:V1 的每个分量 <= V2,且至少一个 < → V1 因果先于 V2
- 并发:V1 和 V2 不可比较(某个分量大,某个小)→ 并发关系
3. 混合逻辑时钟(HLV,Hybrid Logical Clock):
- 结合物理时钟和逻辑时钟
- 格式:(物理时间, 逻辑计数)
- 既保留物理时间的可读性,又保证因果序
- Google Spanner 的 TrueTime 更进一步,用原子钟 + GPS 保证物理时钟的精确度
应用场景:
| 场景 | 方案 |
|---|---|
| 数据库冲突检测 | 向量时钟(DynamoDB、Riak) |
| 分布式因果排序 | Lamport 逻辑时钟 |
| 分布式数据库 | TrueTime / HLC(Spanner、CockroachDB) |
| 消息排序 | 逻辑时钟( Lamport 排序) |
DynamoDB 冲突解决示例:
- 客户端 A 写 key=X,V=[1,0]
- 客户端 B 写 key=X,V=[0,1]
- 两个写并发(向量时钟不可比较)→ 冲突
- 解决策略:最后写入胜(LWW)、读时合并、应用层解决
追问延伸:
- Google Spanner 的 TrueTime 怎么实现物理时钟的精确同步?(原子钟 + GPS)
- 为什么不用 NTP?(NTP 毫秒级误差,不满足线性一致性的要求)
Q18: 什么是分布式系统?和微服务有什么区别? 「🟢 校招/初级」
考察点:分布式基础概念、分布式与微服务的辨析。 参考答案:
什么是分布式系统:
- 多台独立计算机通过网络协同工作,对用户呈现为一个整体
- 目的:
- 高并发/高性能:水平扩容,多台机器分担流量
- 高可用:部分节点宕机不影响整体服务(容灾)
- 可扩展:按需增加机器,不受单机上限约束
- 核心挑战:数据一致性(网络不可靠导致消息丢失/延迟/乱序)
分布式 vs 微服务:
| 维度 | 分布式 | 微服务 |
|---|---|---|
| 本质 | 部署方式(物理层面) | 架构思想(逻辑层面) |
| 关注点 | 机器之间如何协同 | 业务如何拆分 |
| 解决问题 | 单机性能/可用性瓶颈 | 单体应用的代码耦合、团队协作 |
| 关系 | 微服务天然需要分布式部署 | 分布式不一定用微服务 |
理解要点:
- 分布式是物理拆分:多台机器通过网络协同
- 微服务是逻辑拆分:按业务边界拆成独立服务
- 多台机器跑同一个单体应用 → 分布式但不是微服务
- 微服务部署在单台机器上 → 微服务但不是严格分布式(实践中不会这么做)
- 微服务天然需要分布式部署
演进路径:
单体应用 → 垂直拆分(按业务拆成独立单体)→ 分布式部署(多机容灾)
→ 微服务化(进一步按业务边界细粒度拆分)→ 云原生(容器化 + 自动伸缩)追问延伸:
- 分布式系统的核心难题是什么?(网络不可靠、节点可能宕机、时钟不同步)
- SOA 和微服务的区别?(SOA 用 ESB 总线,微服务用轻量级 API 网关)
Q19: 什么情况下需要使用分布式事务? 「🟡 中级」
考察点:分布式事务的使用场景判断。 参考答案:
需要分布式事务的两种典型场景:
场景一:微服务跨服务协同
用户下单 → 订单服务(创建订单)→ 库存服务(扣减库存)→ 账户服务(扣减余额)- 三个操作分布在三个独立数据库中
- 必须保证三个操作要么全成功,要么全回滚
- 不能出现"扣了库存但没扣余额"的情况
场景二:分库分表后跨库操作
订单表按 user_id 分片到不同库
用户 A 的订单在 DB1,用户 B 的订单在 DB2
转账操作需要同时修改两个用户的订单 → 跨库事务业界共识:能不用分布式事务就不用
原因:
- 强一致性分布式事务(2PC/3PC)性能极差(同步阻塞、协调者单点)
- 业务上可以通过设计规避:
- MQ + 本地消息表:主业务完成后发 MQ,异步处理下游,最终一致
- TCC 补偿:业务层面做 try/confirm/cancel,而非数据库层面
- Saga 长事务:拆成多个本地事务,失败时反向补偿
什么时候必须用强一致:
- 金融转账(钱不能多也不能少)
- 库存扣减(超卖问题)
- 这些场景用 TCC 或 Seata AT 模式
什么时候可以最终一致:
- 订单状态通知(延迟几秒可以接受)
- 积分发放(延迟发放不影响核心流程)
- 消息通知(异步即可)
- 这些场景用 MQ 事务消息或本地消息表
设计原则:
优先级:本地事务 > 最终一致(MQ/消息表)> 补偿(TCC/Saga)> 强一致(2PC)追问延伸:
- 下单扣库存的场景,用最终一致还是强一致?(通常用 TCC 或 Redis 预扣 + 异步扣减)
- 分库分表后怎么避免分布式事务?(合理设计分片键,让同一用户的操作落在同一个库)
Q20: Seata 框架的原理是什么?支持哪些模式? 「🔴 高级」
考察点:分布式事务框架的工程实践。 参考答案:
Seata(Simple Extensible Autonomous Transaction Architecture):阿里开源的分布式事务框架。
核心架构——三个角色:
- TC(Transaction Coordinator):事务协调者,独立部署,维护全局事务和分支事务的状态,决定提交或回滚
- TM(Transaction Manager):事务管理器,定义全局事务的开始、提交、回滚(即业务发起方)
- RM(Resource Manager):资源管理器,管理本地事务(即每个微服务的数据库)
流程:
1. TM 向 TC 注册全局事务,获得 XID(全局事务ID)
2. TM 调用各微服务,XID 通过 RPC header 传递
3. 各 RM 收到 XID → 执行本地事务 → 向 TC 注册分支事务 → 本地事务暂不提交
4. TM 收到所有分支的执行结果 → 向 TC 发起提交/回滚
5. TC 通知所有 RM 提交或回滚Seata 支持的四种模式:
1. AT 模式(默认推荐,无侵入):
- 原理:基于 SQL 解析自动生成 undo_log(回滚日志)
- 流程:
- RM 执行本地 SQL 前,先解析 SQL 生成 before image(修改前数据)
- 执行业务 SQL
- 生成 after image(修改后数据)
- 将 undo_log 存入 undo_log 表
- 本地事务提交(业务 SQL + undo_log 在同一个本地事务中)
- 全局提交 → 删除 undo_log
- 全局回滚 → 根据 undo_log 反向补偿
- 特点:业务零侵入,自动补偿,但只支持关系型数据库
- 限制:需要表有主键,不支持复杂嵌套 SQL
2. TCC 模式(手动补偿):
- 业务需实现三个方法:
try:预留资源(如冻结余额)confirm:确认操作(扣减冻结的余额)cancel:取消操作(解冻余额)
- 特点:性能好、适合高并发,但开发成本高,需处理空回滚、悬挂等问题
- 适用:金融、库存扣减等核心场景
3. SAGA 模式(长事务):
- 将长事务拆成 N 个本地事务 + N 个补偿事务
- 正向执行:T1 → T2 → T3 → ...
- 回滚:T3 补偿 → T2 补偿 → T1 补偿
- 特点:适合业务流程长、参与者多的场景(如保险理赔)
- 注意:补偿操作可能不完美(已发送的邮件无法撤回)
4. XA 模式(标准协议):
- 基于 XA 协议的 2PC
- 特点:强一致,但性能差(长时间锁定资源)
- 适用:对一致性要求极高、并发量不大的场景
四种模式对比:
| 维度 | AT | TCC | SAGA | XA |
|---|---|---|---|---|
| 侵入性 | 无 | 高 | 中 | 无 |
| 一致性 | 最终一致 | 最终一致 | 最终一致 | 强一致 |
| 性能 | 高 | 很高 | 中 | 低 |
| 复杂度 | 低 | 高 | 中 | 低 |
| 适用 | 大多数业务 | 高并发核心 | 长流程 | 传统金融 |
追问延伸:
- AT 模式怎么处理脏写?(全局锁:执行前先获取全局锁,避免其他全局事务修改同一行)
- TCC 的空回滚和悬挂是什么?(空回滚:cancel 没有对应的 try;悬挂:try 在 cancel 之后到达)
Q21: ZooKeeper 的核心原理和应用场景是什么? 「🔴 高级」
考察点:ZooKeeper 作为分布式协调服务的全面理解。 参考答案:
ZooKeeper 是什么:
- 分布式协调服务,提供一致性管理(配置管理、命名服务、分布式锁、Leader 选举)
- CP 模型(ZAB 协议保证强一致性)
- 数据模型:树形 Znode 结构,类似文件系统
Znode(数据节点):
| 类型 | 说明 | 场景 |
|---|---|---|
| 持久节点 | 创建后一直存在,直到显式删除 | 配置存储 |
| 持久顺序节点 | 持久 + 自动追加递增序号 | 分布式锁(公平锁) |
| 临时节点 | 客户端 Session 断开自动删除 | 服务注册、Leader 选举、分布式锁 |
| 临时顺序节点 | 临时 + 自动递增 | 公平分布式锁、Leader 选举 |
Watcher(监听机制):
- 客户端可以注册对 Znode 的监听
- Znode 变化时(创建/删除/数据变更)→ 通知客户端
- 一次性触发:触发后监听失效,需重新注册
- 应用:服务上下线感知、配置变更通知、锁释放通知
ZAB 协议(详见 Q8):
- 消息广播:Leader → Follower,过半 ACK 提交
- 崩溃恢复:Leader 宕机 → 选举新 Leader → 数据同步
ZooKeeper 应用场景:
1. 配置管理:
/config
/config/db_url → "mysql://1.2.3.4:3306"
/config/timeout → "3000"- 配置存在 ZK 节点上
- 应用启动时读取 + 注册 Watcher
- 配置变更 → ZK 通知所有应用
2. 服务注册与发现:
/services
/services/order-service
/services/order-service/instance1 → "10.0.0.1:8080" (临时节点)
/services/order-service/instance2 → "10.0.0.2:8080" (临时节点)- 服务启动 → 创建临时节点 → 服务下线/宕机 → 临时节点自动删除
- 消费者注册 Watcher → 实例变化时收到通知
3. 分布式锁(详见 Q12):
/locks/order/00000001 (临时顺序节点)
/locks/order/00000002 (临时顺序节点)- 创建临时顺序节点 → 序号最小者获得锁
- 监听前一个节点删除 → 前一个释放后自己获得锁
- 临时节点保证客户端宕机后锁自动释放
4. Leader 选举:
/election/00000001 → 节点 A (序号最小 → A 是 Leader)
/election/00000002 → 节点 B (监听 00000001)
/election/00000003 → 节点 C (监听 00000002)- 所有参与节点创建临时顺序节点
- 序号最小者成为 Leader
- 非 Leader 节点监听前一个节点
- Leader 宕机 → 临时节点删除 → 下一个节点成为 Leader
5. 命名服务:
- 利用 Znode 的顺序特性生成唯一 ID
- 创建顺序节点 → ZK 返回唯一序号
ZooKeeper 的局限性:
- 写入性能不高(每次写要过半同步,不适合高写入场景)
- 不适合存储大量数据(Znode 默认 1MB 限制)
- 运维复杂(需维护 ZK 集群)
- 新项目越来越多转向 Etcd(Raft + gRPC,K8s 原生支持)
ZooKeeper vs Etcd:
| 维度 | ZooKeeper | Etcd |
|---|---|---|
| 协议 | ZAB | Raft |
| 数据模型 | 树形 Znode | KV |
| API | 自定义 TCP | gRPC + REST |
| 语言 | Java | Go |
| 生态 | Hadoop/Kafka旧版 | Kubernetes |
| Watch | 一次性 | 持续 Watch |
| 存储 | 内存 + 快照 | BoltDB |
追问延伸:
- Kafka 早期用 ZooKeeper 做什么?(Broker 注册、Leader 选举、Consumer Group 协调,2.8 后去 ZK 化)
- ZooKeeper 的羊群效应是什么?(大量 Watcher 同时触发,用顺序节点优化)