Skip to content

分布式理论

系统设计面试的起点:先理解分布式的基本难题,才能谈方案。

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 三大核心机制:

  1. Leader 选举

    • 节点角色:Leader(处理请求)、Follower(被动接收)、Candidate(竞选者)
    • 选举超时(150-300ms 随机):Follower 超时未收到 Leader 心跳 → 变为 Candidate
    • Candidate 增加任期号(term),发起投票请求(RequestVote RPC)
    • 收到多数节点投票 → 成为 Leader
    • Leader 定期发心跳(AppendEntries RPC)维持地位
    • 防止脑裂:每个 term 每个节点只投一票(先到先得)
  2. 日志复制

    • Leader 收到客户端请求 → 写入本地日志(uncommitted)
    • Leader 通过 AppendEntries RPC 把日志发给所有 Follower
    • 多数 Follower 确认后 → Leader 标记为 committed → 返回客户端
    • Leader 在下次心跳中通知 Follower 也 commit
    • 保证:已 commit 的日志不会丢失、所有节点状态一致
  3. 安全性保证

    • 选举限制: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 流程:

  1. Prepare 阶段
    • Proposer 生成全局唯一递增的编号 N,发送 Prepare(N) 给所有 Acceptor
    • Acceptor 收到 Prepare(N):
      • 如果 N > 已承诺的最大编号 → 承诺不再接受 < N 的提议,返回之前已接受的提议
      • 否则 → 拒绝
  2. Accept 阶段
    • Proposer 收到多数 Acceptor 的承诺 → 发送 Accept(N, value)
    • value = 收到的已接受提议中编号最大的值(如果没有则用自己的值)
    • Acceptor 收到 Accept(N, value):
      • 如果 N >= 已承诺的最大编号 → 接受,记录提议
      • 否则 → 拒绝
  3. Learn 阶段
    • Acceptor 接受后通知 Learner

Multi-Paxos(实际使用):

  • Basic Paxos 每次达成一致需要两轮 RPC,效率低
  • Multi-Paxos:选出一个稳定的 Leader,Leader 直接发 Accept(省去 Prepare)
  • Leader 失效后重新选举
  • 但 Multi-Paxos 没有具体实现规范(Raft 是 Multi-Paxos 的具体实现)

Paxos vs Raft 对比:

维度PaxosRaft
理解难度
Leader可有可无强 Leader
日志可乱序连续有序
工程实现少(Chubby)多(Etcd/TiKV)
理论完备最完备完备但简化

追问延伸

  • 为什么 Paxos 难以实现?(协议描述抽象,很多细节未定义)
  • Google Chubby 用的是 Paxos 还是 Raft?(Paxos)

Q8: ZAB 协议是什么?和 Raft 有什么区别? 「🔴 高级」

考察点:ZooKeeper 共识协议的理解。 参考答案

ZAB(ZooKeeper Atomic Broadcast):ZooKeeper 的核心协议,保证主备数据一致性。

ZAB 的两个阶段:

  1. 崩溃恢复(Leader 选举)

    • 节点状态:LOOKING(选举中)、FOLLOWING、LEADING、OBSERVING
    • 选举流程:
      • 每个节点投自己(myid + zxid)
      • 收到其他节点的投票,比较(zxid 大的优先,zxid 相同则 myid 大的优先)
      • 收到多数投票 → 成为 Leader
    • 和 Raft 的区别:Raft 用 term(任期),ZAB 用 zxid(事务ID,含 epoch)
  2. 消息广播(原子广播)

    • 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 对比:

维度ZABRaft
应用ZooKeeperEtcd/Consul/TiKV
数据模型树形(znode)KV
日志只同步新增全量同步
Leader选举zxid 优先term + 日志完整性
读一致性默认读 Leader(强一致)默认线性一致

追问延伸

  • ZooKeeper 为什么选 ZAB 不用 Raft?(历史原因,ZK 2008 年发布时 Raft 还没出现)
  • ZAB 的 zxid 和 Raft 的 term 有什么区别?

Q9: 如果让你设计一个 RPC 框架,怎么设计? 「🔴 高级」

考察点:系统设计能力 + 对 RPC 原理的理解。 参考答案

RPC 框架的核心组件:

  1. 客户端(Consumer)

    • 代理层(Proxy):生成接口的动态代理,让调用远程方法像调用本地方法
    • 负载均衡:从注册中心拿到多个 Provider 地址,选择一个(轮询/随机/一致性哈希/最少连接)
    • 集群容错:失败重试、失败快速失败、失败自动切换、并行调用
    • 序列化:把方法参数序列化为二进制(JSON/Protobuf/Hessian/Kryo)
  2. 网络通信层

    • 通信协议:自定义协议(魔数 + 版本 + 序列化类型 + 消息ID + 数据长度 + 数据体)
    • IO 模型:Netty(NIO + epoll)、长连接复用
    • 编解码:序列化/反序列化 + 拆包粘包处理
  3. 服务端(Provider)

    • 请求分发:根据接口名+方法名找到对应的实现类和方法
    • 反射调用:通过反射执行目标方法
    • 线程池:业务线程池处理请求(和 IO 线程隔离)
  4. 注册中心

    • 服务注册:Provider 启动时注册(接口→地址列表)
    • 服务发现:Consumer 启动时订阅,缓存到本地
    • 健康检查:心跳检测,下线不健康节点
    • 选型:Nacos/ZooKeeper/Eureka/Consul
  5. 治理能力

    • 熔断降级:下游不可用时快速失败 + 降级逻辑
    • 限流:控制请求速率
    • 链路追踪: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 服务,客户说请求超时了,怎么排查? 「🟡 中级」

考察点:线上问题排查的综合能力。 参考答案

排查链路(从外到内):

  1. 确认问题

    • 是偶发还是必现?超时多久?哪个接口?哪个客户?
    • 客户端网络是否正常?是否最近上线了变更?
  2. 网络链路

    • DNS 解析是否正常(dig/nslookup
    • 网络连通性(ping/traceroute
    • 防火墙/安全组是否放通
    • CDN/WAF 是否有延迟
    • 客户端到服务端的网络 RTT
  3. 网关层

    • Nginx/网关日志:看请求是否到达、响应时间、上游状态
    • 网关限流是否触发(429)
    • 网关超时配置是否合理(proxy_read_timeout)
  4. 应用层

    • APM 工具(SkyWalking/Zipkin):看请求在哪个环节慢
    • 应用日志:看请求处理时间、有无异常
    • 是否有慢 SQL、慢 RPC 调用、锁等待
    • GC 是否频繁(jstat -gc
  5. 下游依赖

    • 数据库:慢查询、锁等待、连接池满
    • 缓存:Redis 慢查询、大 Key、热 Key
    • 消息队列:消费积压
    • 第三方 API:外部服务超时
  6. 系统资源

    • CPU/内存/负载是否正常
    • 网络带宽是否打满
    • 磁盘 IO 是否瓶颈
    • 文件句柄/线程数是否打满
  7. 定位根因

    • 全链路追踪(TraceId 贯穿所有服务)
    • 对比正常时的耗时分布,找出异常环节

常见原因排序:

  1. 慢 SQL(最常见,索引缺失、全表扫描)
  2. 下游 RPC 超时(级联超时)
  3. GC 停顿(Full GC / STW)
  4. 锁竞争(synchronized / 数据库行锁)
  5. 网络问题(跨机房、DNS、CDN)
  6. 资源耗尽(连接池满、线程池满)

追问延伸

  • 怎么设置合理的超时时间?(连接超时 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
end

Redis 锁的三个问题

  • 锁过期但业务没执行完:看门狗(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 租约防死锁、版本号实现公平锁

方案对比

维度RedisZooKeeperEtcd
一致性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) 轮后,数据传播到全网

三种传播模式:

  1. Push:有数据的节点主动告诉别人(快速但后期浪费带宽)
  2. Pull:没数据的节点主动问别人(后期效率高)
  3. 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 有什么区别? 「🟡 中级」

考察点:服务发现方案选型能力。 参考答案

四个主流注册中心对比:

维度NacosEurekaZooKeeperConsul
CAP 模型AP + CPAPCPCP
一致性协议Raft + Distro无(P2P 复制)ZABRaft
健康检查TCP/HTTP/心跳心跳Session/临时节点TCP/HTTP/Script
多数据中心支持(集群)不支持不支持原生支持
配置中心一体化KV 存储
使用语言JavaJavaJavaGo
维护状态活跃停止维护(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(回滚日志)
  • 流程:
    1. RM 执行本地 SQL 前,先解析 SQL 生成 before image(修改前数据)
    2. 执行业务 SQL
    3. 生成 after image(修改后数据)
    4. 将 undo_log 存入 undo_log 表
    5. 本地事务提交(业务 SQL + undo_log 在同一个本地事务中)
    6. 全局提交 → 删除 undo_log
    7. 全局回滚 → 根据 undo_log 反向补偿
  • 特点:业务零侵入,自动补偿,但只支持关系型数据库
  • 限制:需要表有主键,不支持复杂嵌套 SQL

2. TCC 模式(手动补偿)

  • 业务需实现三个方法:
    • try:预留资源(如冻结余额)
    • confirm:确认操作(扣减冻结的余额)
    • cancel:取消操作(解冻余额)
  • 特点:性能好、适合高并发,但开发成本高,需处理空回滚、悬挂等问题
  • 适用:金融、库存扣减等核心场景

3. SAGA 模式(长事务)

  • 将长事务拆成 N 个本地事务 + N 个补偿事务
  • 正向执行:T1 → T2 → T3 → ...
  • 回滚:T3 补偿 → T2 补偿 → T1 补偿
  • 特点:适合业务流程长、参与者多的场景(如保险理赔)
  • 注意:补偿操作可能不完美(已发送的邮件无法撤回)

4. XA 模式(标准协议)

  • 基于 XA 协议的 2PC
  • 特点:强一致,但性能差(长时间锁定资源)
  • 适用:对一致性要求极高、并发量不大的场景

四种模式对比

维度ATTCCSAGAXA
侵入性
一致性最终一致最终一致最终一致强一致
性能很高
复杂度
适用大多数业务高并发核心长流程传统金融

追问延伸

  • 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

维度ZooKeeperEtcd
协议ZABRaft
数据模型树形 ZnodeKV
API自定义 TCPgRPC + REST
语言JavaGo
生态Hadoop/Kafka旧版Kubernetes
Watch一次性持续 Watch
存储内存 + 快照BoltDB

追问延伸

  • Kafka 早期用 ZooKeeper 做什么?(Broker 注册、Leader 选举、Consumer Group 协调,2.8 后去 ZK 化)
  • ZooKeeper 的羊群效应是什么?(大量 Watcher 同时触发,用顺序节点优化)