分布式面试很少停留在背定义:问完 CAP 必追问取舍场景,问完锁必追问超时事故。本文按理论、通信、ID 与锁与事务、协调组件四条主线整理高频题,每题给出结论、原理与加分延伸。

考察地图

  • 分布式理论:CAP 的真实取舍、BASE 如何落地、Raft 的选举与复制。考察能否把理论映射到注册中心选型等具体场景。
  • RPC 与通信:RPC 与 HTTP 之争。考察对协议、序列化与服务治理能力的整体理解。
  • ID、锁与事务:Snowflake 的结构与缺陷、Redis 锁三部曲(过期时间、Lua 释放、看门狗)、四种事务方案选型。这是追问最密集的区域。
  • 协调组件:ZooKeeper 的临时节点、顺序节点、Watcher 三件套。
  • 建议自行查阅:Paxos 流程、Gossip 细节、一致性哈希与虚拟节点、配置中心推拉模型——广度题,优先级低于主线。

分布式理论

CAP 真的是「三选二」吗?

结论:不是三选二。网络分区在分布式环境中必然发生,P 无法放弃,真正的取舍只发生在分区期间:保一致性(CP)还是保可用性(AP)。

原理:C 指线性一致——所有节点同一时刻看到同一份数据;A 指每个请求在有限时间内得到非错误响应;P 指网络断成几片时系统仍能运转。光纤会被挖断、交换机会宕机,所以 P 是前提而非选项。分区出现时二选一:继续对外服务但可能返回旧数据(AP,如 Eureka、Cassandra);或拒绝部分请求直到副本达成一致(CP,如 ZooKeeper、etcd)。分区恢复后系统可回到 CA 兼得状态;PACELC 进一步指出,无分区时还要在延迟与一致性之间权衡。

延伸:注册中心是最好的例子——Eureka 自我保护宁可保留过期实例也不清空列表,是 AP;ZooKeeper 分区后少数派不可用,是 CP。

追问:BASE 和 CAP 是什么关系?提示:BASE 是 AP 路线的工程化表述,基本可用、软状态、最终一致三词分别对应什么妥协。

BASE 和 ACID 的区别是什么?最终一致性怎么落地?

结论:ACID 追求单库内的强一致;BASE 接受中间状态,用时间换取一致性收敛,是放弃强一致后的落地哲学,二者是取舍而非对错。

原理:ACID 靠锁与 redo/undo 日志在单节点保证事务;跨节点后强一致代价陡增(2PC 阻塞、可用性下降),BASE 转而允许副本短暂不一致,靠异步机制逐步收敛。落地工具箱:本地消息表或事务消息保证「写库 + 发消息」原子;消费端幂等加无限重试;定时对账任务扫描中间态单据;状态机驱动 Saga 补偿。软状态的典型设计是订单超时未支付自动关单——中间态显式存在,且有明确的生命周期。

延伸:最终一致不等于「不一致也没关系」:要定义可接受的收敛时间并配监控,消息堆积超阈值、对账差异不为零都应告警介入。

追问:怎么向面试官说明你们系统的收敛时间?提示:从消息堆积监控、对账任务频率和业务可容忍窗口三个角度回答。

Raft 为什么比 Paxos 容易理解?讲讲选举和复制。

结论:Raft 把共识拆解成 Leader 选举、日志复制、安全性三个相对独立的子问题,并用强 Leader 收敛状态空间,因此比 Multi-Paxos 容易理解得多,也容易实现得对。

原理:节点只有 Follower、Candidate、Leader 三种状态。Follower 在随机选举超时内收不到心跳就转为 Candidate 拉票,获得多数派即当选——随机超时让分票概率极低。所有写请求走 Leader:先本地追加日志,再并行复制给 Follower,条目被多数节点持久化即提交,之后应用到状态机并通过心跳广播提交位点。安全性靠两条铁律:只有日志不比选民旧的候选者才能拿到选票;Leader 只追加不覆盖,Follower 日志冲突时以 Leader 为准截断重写。

延伸:etcd、Consul、TiKV 都采用 Raft 或其变体;ZooKeeper 的 ZAB 与 Raft 思想相近,但面向主备复制与崩溃恢复做了自己的取舍。

追问:分区时少数派一侧的旧 Leader 会怎样?提示:任期号单调递增,旧 Leader 的复制拿不到多数派,重新连入集群后收到更高任期心跳即降级。

RPC 与通信

RPC 和 HTTP 有什么区别?为什么内部调用常用 RPC?

结论:HTTP 是通用超文本协议,RPC 是「像调本地方法一样调远程服务」的编程模型;内部服务间调用偏好 RPC,胜在报文更紧凑、契约更强、治理能力内置。

原理:主流 RPC 框架(Dubbo、gRPC)使用自定义二进制协议或 HTTP/2 + Protobuf,序列化后体积远小于 JSON 文本,编解码 CPU 开销也更低。接口由 IDL 定义并生成强类型桩代码,省掉拼 URL、手写序列化的样板。更重要的是框架内置注册发现、负载均衡、超时重试、熔断等治理组件,裸 HTTP 客户端都得自己拼。注意现代语境下差距在缩小:gRPC 本就跑在 HTTP/2 上,准确对比应是「二进制 RPC vs 文本 REST」。

延伸:对外 API 仍以 HTTP/REST 为主,浏览器友好、调试工具成熟;Mesh 把治理下沉到 sidecar 后,协议之争进一步淡化。

追问:一次 RPC 调用经过哪些环节?提示:序列化、网络传输、反序列化、服务发现、负载均衡、超时与重试控制,逐一说出各环节的坑。

分布式 ID、锁与事务

分库分表后为什么不能再用数据库自增 ID?Snowflake 怎么设计的?

结论:单库自增依赖单点递增,分库后各库独立自增必然撞号,且无法全局趋势递增;Snowflake 用「时间戳 + 机器位 + 序列号」在本地拼出 64 位趋势递增 ID,无需中心节点。

原理:经典 Snowflake 划分为 1 位符号位 + 41 位毫秒时间戳(约 69 年)+ 10 位机器位(1024 节点)+ 12 位序列号(单毫秒 4096 个)。生成纯本地运算,性能极高;时间戳占高位保证整体趋势递增,对 B+ 树索引友好,避免页分裂。代价是对时钟敏感:时钟回拨会产生重复 ID,工程上靠启动时校验、小回拨等待、大回拨拒绝服务并告警来兜底;机器位应通过注册中心或 ZK 动态分配,不能硬编码。

延伸:号段模式(如 Leaf-segment)每次从 DB 批量取一段号缓存本地,把 DB 压力降为原来的 1/步长,且不强依赖时钟;UUID 无序导致 InnoDB 页分裂,一般不适合做聚簇主键。

追问:时钟回拨有哪些具体处理策略?提示:小回拨自旋等待、大回拨换备用机器位、或用逻辑时钟替代物理时钟。

Redis 分布式锁为什么要设过期时间?为什么释放锁要用 Lua?

结论:过期时间防止持锁者宕机后锁永远泄漏形成死锁;释放锁必须先验证持有者身份再删除,「判断 + 删除」两步必须用 Lua 原子执行,否则会误删别人的锁。

原理:加锁用 SET key value NX PX ttl,value 存唯一标识(UUID + 线程 ID)。没有过期时间,客户端崩溃后锁永远无法被他人获取。释放时先 GET 比对 value 再 DEL,这两条命令之间存在窗口:GET 刚通过,锁恰好过期被他人获取,随后 DEL 执行——删掉的已是别人的锁。Lua 脚本利用 Redis 的单线程命令执行模型,把 compare-and-delete 合成一个原子操作,消除该窗口。

String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";

延伸:TTL 需大于业务最长执行时间,但精确预估很难——业务没跑完锁先过期,另一客户端进入并发写,这是 Redis 锁最经典的事故,直接引出看门狗续期方案。

追问:主从切换时 Redis 锁会怎样?提示:异步复制下主宕机时锁记录可能未同步到从,新主上锁丢失,两个客户端同时持锁——这正是 Redlock 之争的核心。

Redisson 看门狗解决了什么问题?锁还有哪些失效路径?

结论:看门狗在持锁线程存活期间周期性续期(默认锁 30 秒、每 10 秒续一次),解决「业务没执行完锁先过期」的问题;但它救不了进程崩溃、长时间 GC 停顿和主从切换丢锁。

原理:Redisson 加锁成功后启动 Netty 定时任务,按看门狗超时时间的 1/3 间隔执行 Lua 续期。只要进程健康,锁不会因 TTL 到期被抢;进程崩溃则续期停止,锁到期自然释放,不产生死锁。失效路径仍有三条:Full GC 或网络抖动让续期停摆,锁过期后他人进入,原持有者还以为持锁;主从异步复制下主宕机丢锁;时钟跳变影响 TTL。对正确性敏感的场景需要 fencing token:持锁时附带单调递增序号,下游拒绝序号小于已见最大值的迟到写。

延伸:Martin Kleppmann 与 Antirez 的 Redlock 争论本质是对时钟与进程停顿假设的分歧。金融级强互斥优先选 ZooKeeper/etcd 锁或数据库唯一约束;Redis 锁适合性能优先、偶发并发可容忍的场景。

追问:fencing token 具体怎么实现?提示:用 Redis INCR 或 ZK 的 zxid 生成序号,下游存储记录最大已处理序号并拒绝回退。

2PC、TCC、Saga、事务消息分别适合什么场景?

结论:2PC 强一致但阻塞,适合短事务与中间件内部;TCC 把资源预留上提到业务层,适合资金类强一致场景;Saga 用补偿换长事务的可用性;事务消息解决「本地事务 + 异步通知」的最终一致。

原理:2PC 由协调者驱动 prepare/commit 两阶段,prepare 后资源被锁定,协调者宕机会让参与者长时间阻塞。TCC 要求业务实现 Try(冻结库存)、Confirm(真正扣减)、Cancel(释放冻结)三个接口,框架负责重试与幂等,一致性好但侵入性强。Saga 把长事务拆为一串本地事务,每步登记补偿动作,失败时逆序执行,适合跨外部系统的长流程。事务消息(RocketMQ)先发 half 消息再执行本地事务,最后二次确认,配合回查机制保证「库写成功则消息必达」。

延伸:选型口诀:要强一致看 TCC,要吞吐看消息表与 Saga。注意补偿不是「回滚到旧值」——已发出的短信无法撤回,只能补发致歉短信,补偿动作必须符合业务语义。TCC 还要处理空回滚与悬挂两个经典坑。

追问:什么是空回滚和悬挂?提示:Try 未执行而 Cancel 先到需放行(空回滚),Cancel 先于 Try 生效后要拒绝迟到的 Try(悬挂),靠事务状态表记录识别。

协调组件

ZooKeeper 的临时节点、顺序节点、Watcher 分别解决什么问题?

结论:临时节点随会话断开自动删除,天然表达「在线状态」;顺序节点带全局单调递增后缀,用于公平锁与选主排序;Watcher 提供一次性事件通知,是配置变更推送与锁等待唤醒的基础设施。

原理:临时节点生命周期绑定会话(session),只有会话超时而非一次心跳失败才删除,避免网络抖动误杀实例——这正是注册中心的语义。抢锁时所有客户端在同一父节点下创建临时顺序节点,序号最小者持锁,其余只监听自己的前一个节点,释放时只唤醒一个等待者,避免羊群效应。Watcher 一次性触发后需重新注册,且只通知「有变化」不带最终数据,重新注册与读取之间存在漏事件窗口,Curator 的 Cache 系列正是为屏蔽此复杂度而生。

延伸:ZK 是 CP,选举期间整体不可写;写请求全经 Leader 原子广播,读可通过 Observer 扩容。适合低频高可靠的协调控制面,不适合当高频数据存储。

追问:用 ZK 做注册中心相比 Eureka 的代价是什么?提示:分区时不可用、客户端要处理会话过期重注册、ZK 集群本身运维成本高。