分布式系统面试高频题
分布式系统面试高频题
分布式系统是高级/资深工程师面试中的核心模块,CAP 理论、分布式事务、一致性算法、分布式 ID 是高频考点。以下 10 题覆盖了从理论到实践的常见面试知识点。
Q1: CAP 理论和 BASE 理论是什么?⭐️⭐️⭐️
难度: ⭐️⭐️⭐️ | 考察点: CAP 定理、BASE 理论、分布式系统权衡
面试官问:"说说 CAP 理论,为什么 CAP 不能同时满足?项目中你是怎么权衡的?"
核心回答
CAP 定理(Eric Brewer,2000):分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可同时满足,最多只能满足两个。
C(一致性):所有节点在同一时刻看到的数据完全一致
A(可用性):每个请求都能收到一个非错误的响应(即使某些节点故障)
P(分区容错性):系统在网络分区(节点间通信中断)时仍能继续运作
三者关系:
CA:舍弃 P → 网络分区时系统无法工作(单机系统)
CP:舍弃 A → 网络分区时放弃可用性保证一致性(ZooKeeper、HBase)
AP:舍弃 C → 网络分区时接受短暂不一致保证可用性(Eureka、Cassandra、DNS)BASE 理论:对 CAP 中 AP 方案的补充。
BA(Basically Available)— 基本可用:允许响应时间变慢或部分功能降级
S(Soft State)— 软状态:允许系统存在中间状态,不要求强一致性
E(Eventually Consistent)— 最终一致性:一段时间后最终达到一致
核心思想:无法做到强一致性,但每个系统都可以根据业务特点,用适当的方式达到最终一致性深度扩展
CAP 的常见误解:
误解 1:CAP 只能选 2 个
→ 真实含义:三者不可同时满足,但可以"牺牲程度不同地满足三个"
→ 例如:正常情况下 C、A、P 都满足;网络分区发生时,选择牺牲 C 或 A
误解 2:CA 系统是好的
→ 分布式系统中 P 是必须面对的(网络分区必然发生)
→ 纯 CA 系统 = 单机系统(不是分布式)
→ 真正的选择只有 CP 或 AP实际场景中的抉择:
| 场景 | 选择 | 理由 |
|---|---|---|
| 银行转账 | CP | 宁可不可用也不能出现余额错误 |
| 电商下单 | CP(扣库存) | 库存不能超卖 |
| 商品详情页 | AP | 允许短暂不一致(缓存延迟),但不能打不开 |
| 微博 Feed 流 | AP | 允许不同用户看到的内容有短暂差异 |
| 支付清算 | CP | 一致性高于一切 |
注册中心 CAP 选型对比:
| 组件 | CAP | 设计 | 后果 |
|---|---|---|---|
| Eureka | AP | 节点平等,网络故障时可用节点继续服务 | 可能拿到已下线的服务实例 |
| ZooKeeper | CP | 半数以上节点存活才提供服务 | 网络分区时部分节点不可用 |
| Nacos | CP + AP 可切换 | 通过 ephemeral 判断临时/持久实例 | 灵活但配置复杂 |
| Consul | CP | 强一致性 | 与 ZK 类似 |
面试追问
Q: 为什么分布式系统必须选择 P? A: 因为在分布式系统中,网络分区是不可避免的——交换机故障、网线松动、网络拥堵都会导致节点间通信中断。P 不是一个"选项",而是分布式系统的本质约束。放弃 P = 回到单机系统。
Q: BASE 和 ACID 的关系? A: ACID(原子性、一致性、隔离性、持久性)是数据库事务的特性,追求强一致性。BASE 是分布式系统的实践总结,追求最终一致性。CAP 中的 C ≈ ACID 中的 C,BASE 是 CAP 中 AP 的落地方法论。
常见错误
- ❌ 说"分布式系统要选 CAP 中的两个"——实际上 P 是必选的,真正的选择是 CP 还是 AP
- ❌ 说 BASE = 放弃了一致性——BASE 是放弃了强一致性,保证了最终一致性
- ❌ 所有场景都选 AP——金融、支付等场景必须选 CP
一句话总结
CAP:P 必须,C 和 A 二选一。BASE = AP 的实践落地(基本可用 + 软状态 + 最终一致性)。选 CP 还是 AP 取决于业务对一致性的要求。
Q2: 分布式事务的解决方案有哪些?⭐️⭐️⭐️
难度: ⭐️⭐️⭐️ | 考察点: 2PC、TCC、Saga、Seata、最终一致性
面试官问:"分布式事务有哪些解决方案?各自的优缺点是什么?实际项目中你是怎么选型的?"
核心回答
| 方案 | 模型 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 2PC(XA) | 强一致 | 强 | 差(资源锁定) | 低 | 单块应用中跨库事务 |
| TCC | 补偿 | 最终 | 好 | 高(需实现 Try/Confirm/Cancel) | 金融、支付核心链路 |
| Saga 编排 | 补偿 | 最终 | 好 | 中(事件驱动或编排) | 长流程业务(下单→支付→发货) |
| 本地消息表 | 消息最终一致 | 最终 | 好 | 低 | 对一致性要求不特别高的业务 |
| RocketMQ 事务消息 | 消息最终一致 | 最终 | 好 | 低 | 异步解耦场景 |
| Seata AT | 自动补偿 | 最终 | 好 | 低(零侵入) | 通用分布式事务 |
| 最大努力通知 | 重试+兜底 | 弱一致 | 好 | 低 | 外部系统对接 |
深度扩展
2PC(两阶段提交):
阶段一(Prepare):
TM(事务管理器)→ RM1: prepare → OK
TM → RM2: prepare → OK
阶段二(Commit/Rollback):
TM → RM1: commit → OK
TM → RM2: commit → OK
致命问题:
- Prepare 阶段后 TM 宕机 → 资源无限期锁定(阻塞所有事务)
- 协调者单点故障
- Commit 阶段部分成功部分失败 → 数据不一致TCC(Try-Confirm-Cancel):
以转账为例:
Try(预留资源):
账户 A:冻结 100 元(余额-100, 冻结+100)
账户 B:(无需冻结)
Confirm(确认执行):
账户 A:冻结-100, 余额不变
账户 B:余额+100
Cancel(回滚):
账户 A:冻结-100, 余额+100(恢复冻结的 100)
账户 B:(无需操作)
优点:无资源锁定(Try 只是预留,不影响其他事务)
缺点:每个接口都要实现三套逻辑,业务侵入性高Seata AT 模式(自动补偿):
Seata AT 一键接入(@GlobalTransactional 注解):
一阶段:
执行业务 SQL → Seata 自动解析 SQL → 保存 undo_log(before image + after image)
二阶段-提交:
异步删除 undo_log(不阻塞)
二阶段-回滚:
根据 undo_log 的 before image 生成反向 SQL → 执行回滚 → 删除 undo_log
优点:业务零侵入,只需一个注解
缺点:仅支持关系型数据库,性能略低于 TCC面试追问
Q: 为什么 2PC 不适用于微服务? A: ① 2PC 的资源锁定时间 = 整个事务执行时间(数据库连接/锁不释放),高并发下不可接受;② 跨服务间的网络延迟和不确定性远大于单应用,协调者宕机导致资源长时间锁定;③ 很多微服务数据源不是数据库(Redis、MQ、文件),无法参与 XA 协议。
Q: TCC 的 Confirm 和 Cancel 需要幂等吗? A: 需要。因为网络超时可能导致 TM 重试,Confirm/Cancel 可能被多次调用。实现幂等的常见方式:① 状态机(Try → 已尝试,Confirm → 已确认,Cancel → 已取消);② 唯一业务 ID 去重。
常见错误
- ❌ 用 2PC 解决微服务间的分布式事务——资源长期锁定,高并发下系统崩溃
- ❌ TCC 忘记实现幂等——网络重试导致重复扣款/重复解冻
- ❌ 所有场景都追求强一致性——绝大多数业务场景下最终一致性够用
一句话总结
金融核心用 TCC(强一致 + 高性能),通用业务用 Seata AT(零侵入)或本地消息表(简单可靠),长流程用 Saga。2PC 只适合单应用跨库,微服务慎用。
Q3: 分布式 ID 生成方案有哪些?⭐️⭐️⭐️
难度: ⭐️⭐️⭐️ | 考察点: 雪花算法、号段模式、数据库自增
面试官问:"分布式系统中如何生成全局唯一 ID?雪花算法有什么优缺点?"
核心回答
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 随机数 + MAC + 时间戳 | 本地生成,无依赖 | 无序(B+ 树插入性能差)、128 位太长 |
| 数据库自增 | AUTO_INCREMENT | 简单,有序,天然递增 | 性能瓶颈,单点故障 |
| 数据库号段模式 | 一次取一批号段(如 1000 个) | 高性能,有序 | 依赖数据库,号段浪费 |
| Redis 自增 | INCR / INCRBY | 快,有序 | 持久化开销,主从切换可能重复 |
| 雪花算法(Snowflake) | 机器 ID + 时间戳 + 序列号 | 高性能、有序、去中心化 | 依赖机器时钟,时钟回拨问题 |
推荐:雪花算法或其变种(美团 Leaf、百度 uid-generator)是目前最主流方案。
深度扩展
雪花算法(Snowflake)结构(64 位长整型):
[1 bit 未使用] [41 bit 时间戳] [10 bit 机器 ID] [12 bit 序列号]
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 00000 - 000000000000
↑ ↑ ↑ ↑ ↑
保留 毫秒级时间戳(69 年) 数据中心 工作节点 序列号(4096/ms)
生成规则:
- 毫秒内时间戳单调递增 → 总体趋势递增
- 毫秒内序列号 0~4095,不够等到下一毫秒
- QPS 上限:4096 × 1000 = 409 万/秒(单机理论值)时钟回拨问题及解决方案:
问题:服务器时钟回拨 → 可能产生重复 ID
解决方案:
1. 等待追平:序列号消耗完且时钟回拨 ≤ 5ms → 等待时钟追平
2. 备用机器 ID:回拨 > 5ms → 切换到备用 workId
3. 号段模式兜底:回拨时直接用号段生成,不用时间戳
4. 直接拒绝:回拨超过阈值 → 拒绝生成并告警
美团 Leaf:
- Leaf-segment(号段模式):从数据库取号段,无时钟依赖
- Leaf-snowflake(雪花模式):通过 ZooKeeper 注册 workId,解决时钟回拨各方案性能对比:
| 方案 | QPS(单机) | 是否趋势递增 | 依赖 |
|---|---|---|---|
| UUID | 本地 >10万 | ❌ 无序 | 无 |
| DB 自增 | ~500 | ✅ | MySQL |
| DB 号段模式 | >10 万(可缓冲) | ✅ | MySQL(低频) |
| Redis INCR | ~5 万 | ✅ | Redis |
| Snowflake | >100 万 | ✅ | 无(需 workId 分配) |
面试追问
Q: 雪花算法生成的 ID 全局唯一吗?会不会冲突? A: 在 workId 唯一 + 时钟正常的前提下,全局唯一。但 workId 重复(手动配置错误)或时钟大幅回拨(超过阈值未处理)可能导致 ID 冲突。生产环境中通过 ZooKeeper / MySQL 自动分配 workId 可以避免人为配置错误。
Q: 为什么 ID 要趋势递增? A: ① 数据库 InnoDB 以主键 B+ 树组织数据,递增 ID 意味着每次插入都在最后(顺序写),页分裂少;② 便于按时间排序和分页(ORDER BY id DESC);③ MySQL 页分裂会降低写入性能并导致索引碎片。
常见错误
- ❌ 直接用 UUID 做主键——MySQL InnoDB 的性能杀手(页分裂、索引碎片)
- ❌ 雪花算法的 workId 随便写死同一个值——多实例产生相同 ID
- ❌ 号段模式每次取 1 个号——退化为 DB 自增,失去性能优势
一句话总结
雪花算法 = 时间戳 + 机器 ID + 序列号,高性能 + 趋势递增 + 无中心依赖。核心难题是时钟回拨——美团 Leaf 用号段+雪花双模式、ZooKeeper 自动分配 workId 解决。
Q4: 如何实现分布式 Session?⭐️⭐️
难度: ⭐️⭐️ | 考察点: 分布式 Session、Sticky Session、Session 共享
面试官问:"现在系统部署了多台服务器,用户的登录状态怎么在多台服务器之间共享?"
核心回答
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Sticky Session(粘性会话) | Nginx/负载均衡器把同一用户路由到同一服务器 | 无需改造 | 服务器宕机则 Session 丢失、负载不均 |
| Session 复制 | 服务器之间互相拷贝 Session(如 Tomcat) | 不依赖外部组件 | 网络开销大、内存浪费(每台存全量) |
| 集中存储(Redis)⭐ | Session 统一存在 Redis 中 | 简单、可靠、高性能 | 依赖 Redis 高可用 |
| JWT 无状态 | 不存 Session,客户端带 Token | 无状态、天然支持 | Token 不可主动失效、payload 不加密 |
| 数据库存储 | Session 存入 MySQL/PostgreSQL | 持久化 | 性能差、增加数据库负载 |
推荐方案:Redis 集中存储 Session(Spring Session 一行配置即可)。
深度扩展
Spring Session + Redis 实现:
// 1. 添加依赖
// spring-session-data-redis
// 2. application.yml
spring:
session:
store-type: redis
timeout: 1800 # session 过期时间(秒)
redis:
host: localhost
port: 6379
// 3. 启动类注解
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
// 原理:
// - HttpServletRequest.getSession() 返回的是 Spring Session 的实现
// - Session 数据自动序列化到 Redis(key: spring:session:<sessionId>)
// - 通过 SessionRepositoryFilter 拦截请求,自动从 Cookie/Header 中提取 sessionIdSticky Session 的 Nginx 配置:
upstream backend {
ip_hash; # 按客户端 IP Hash,同一 IP 始终路由到同一服务器
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}面试追问
Q: IP Hash 方式的 Sticky Session 有什么问题? A: ① 同一局域网的多个用户(相同公网 IP)被路由到同一台服务器 → 负载不均;② 某台服务器宕机 → 其上的所有 Session 丢失;③ 加服务器时 Hash 结果变化,已有 Session 会"迁移"到新服务器。
Q: JWT Token 模式下如何实现"踢人下线"? A: JWT 天生无法主动失效。解决方案:① 维护一个 Redis 黑名单(需要踢的用户 ID/Token ID 加入黑名单,验证时先查黑名单);② 短 Access Token(15 分钟) + Refresh Token,更改状态后让旧的 Access Token 自然过期,同时废弃 Refresh Token。
常见错误
- ❌ Sticky Session 不配
ip_hash却期望会话保持——Nginx 默认是轮询,不走 ip_hash 不会保持会话 - ❌ Redis 主从切换时 Session 丢失——Redis 故障期间 Session 无法读写,建议 Redis 集群 + AOF 持久化
- ❌ 各种方案都上 Redis + JWT + Session 混合——增加复杂度,尽量统一认证方案
一句话总结
分布式 Session:最简单可靠的是 Redis 集中存储(Spring Session 零代码接入);JWT 适合微服务和跨域场景但无法主动失效需黑名单兜底;Sticky Session 和 Session 复制通常不推荐。
Q5: 什么是服务雪崩?如何熔断和降级?⭐️⭐️
难度: ⭐️⭐️ | 考察点: 服务雪崩、熔断、降级、限流、Sentinel
面试官问:"什么是服务雪崩?你是怎么通过熔断和降级来防止雪崩的?"
核心回答
服务雪崩:微服务架构中,一个服务故障 → 调用它的上游服务超时等待 → 上游服务的线程/连接池被耗尽 → 整个调用链崩溃。
雪崩链:
Service D(慢/挂)
→ Service C 调用 D 超时(线程阻塞等待)
→ Service B 调用 C 超时
→ Service A 调用 B 超时
→ 网关超时 → 全链路崩溃三道防线:
| 防线 | 机制 | 作用 | 工具 |
|---|---|---|---|
| 限流 | 控制请求量,超过阈值直接拒绝 | 防患未然 | Sentinel、Guava RateLimiter |
| 熔断 | 错误率达到阈值,直接"断开"故障服务 | 快速失败,不拖死上游 | Hystrix、Sentinel、Resilience4j |
| 降级 | 返回兜底数据或逻辑(不调用故障服务) | 保证核心链路可用 | 同熔断框架 |
深度扩展
熔断器状态机:
请求失败次数达标
↓
CLOSED ──────────→ OPEN
(正常) 熔断开启 (断路)
↑ ↓
│ 等待时间窗口(如 10s)后
│ ↓
└────────── HALF-OPEN
尝试成功 (半开:放少量请求试探)
↓ 试探失败
→ OPEN(再次熔断)Sentinel 与 Hystrix 对比:
| 特性 | Hystrix | Sentinel |
|---|---|---|
| 维护状态 | 停止维护 | 活跃维护 |
| 隔离策略 | 线程池/信号量 | 信号量(轻量) |
| 熔断降级 | 异常比例 | 异常比例 + 慢调用比例 + 异常数 |
| 控制台 | Dashboard | Sentinel Dashboard(功能更强) |
| 规则推送 | 无 | 支持动态规则推送 |
| 自适应 | 无 | 系统自适应保护(Load、CPU) |
Sentinel 三种熔断策略:
// 1. 慢调用比例:响应时间超阈值的请求超过比例 → 熔断
// 1 秒内有 5 个慢调用,且慢调用比例 > 50% → 熔断
// 2. 异常比例:异常比例超过阈值 → 熔断
// 1 秒内异常比例 > 50% → 熔断
// 3. 异常数:一分钟内异常数超过阈值 → 熔断和限流的区别:
| 限流 | 熔断 | |
|---|---|---|
| 触发条件 | QPS / 并发线程数超过阈值 | 错误率 / 慢调用超过阈值 |
| 作用对象 | 自己(控制被调频率) | 下游依赖(检测下游健康) |
| 行为 | 超出部分排队或拒绝 | 直接拒绝调用故障下游 |
| 目的 | 自我保护 | 防止级联故障 |
面试追问
Q: Hystrix 线程池隔离和信号量隔离有什么区别? A: 线程池隔离:每个依赖服务独立的线程池,不相互影响,但线程池切换有开销。信号量隔离:轻量,基于计数器,无线程切换,但不支持超时(调用的线程本身需要能超时退出)。
Q: 降级和熔断的关系? A: 熔断是触发条件(发现下游挂了),降级是执行动作(返回兜底数据)。通常配合使用:熔断开启 → 执行降级逻辑。但降级也可以单独触发(如双十一时主动关闭非核心服务)。
常见错误
- ❌ 所有服务都不设超时时间——一个服务慢 3 秒,整个调用链被拖慢
- ❌ 熔断后立即重试——应该等一段时间(Half-Open 时间窗口)再试探
- ❌ 降级返回 null——应该返回有意义的兜底数据(如空列表、缓存数据),避免 NPE
一句话总结
雪崩 = 服务级联故障。三道防线:限流(控制量) → 熔断(快速失败) → 降级(兜底保核心)。Sentinel 是 Java 生态的首选熔断限流框架。
Q6: 负载均衡算法有哪些?Nginx 和 Ribbon 的异同?⭐️⭐️
难度: ⭐️⭐️ | 考察点: 负载均衡算法、服务端 vs 客户端负载均衡
面试官问:"说说常见的负载均衡算法。Nginx 和 Ribbon 的负载均衡有什么不同?"
核心回答
| 算法 | 原理 | 场景 |
|---|---|---|
| 轮询(Round Robin) | 依次分配 | 各服务器性能相近 |
| 加权轮询(Weighted RR) | 按权重比例分配 | 服务器性能不同 |
| 最少连接数(Least Connections) | 选当前连接最少的 | 长连接场景 |
| 最少活跃调用(Least Active) | 选活跃请求数最少的 | 处理时间不一致的请求 |
| 一致性 Hash | 相同参数值路由到同一服务器 | 缓存命中、会话保持 |
| 随机(Random) | 随机选择 | 简单、各服务器相当 |
| 源地址哈希(IP Hash) | 客户端 IP 哈希 | 会话保持 |
Nginx vs Ribbon:
| 对比维度 | Nginx(服务端负载均衡) | Ribbon(客户端负载均衡) |
|---|---|---|
| 位置 | 独立部署,在服务集群前面 | 嵌入在客户端/服务调用方内 |
| 服务列表 | 静态配置或动态注册 | 从注册中心(Eureka)拉取 |
| 配置方式 | nginx.conf | 代码注解 + 配置文件 |
| 故障转移 | 手动或 proxy_next_upstream | 自动剔除故障节点(注册中心感知) |
| 灵活性 | 较弱(通用代理) | 强(定制策略、与 Spring 深集成) |
| 性能 | 较高(C 语言) | 一般(Java 语言) |
深度扩展
加权轮询的平滑算法(Nginx SWRR):
三个服务器权重 = {5, 1, 1}
每次选择当前有效权重(current_weight)最大的服务器:
初始 effective_weight = 5, 1, 1
current_weight = 0, 0, 0
第 1 次:cur = {5, 1, 1} → 选 A → cur = {5-7=-2, 1, 1} → cur = {-2, 1, 1}
第 2 次:cur = {3, 2, 2} → 选 A → cur = {-4, 2, 2}
第 3 次:cur = {1, 3, 3} → 选 B → cur = {1, -4, 3}
... 7 次内:A 被选中 5 次,B 1 次,C 1 次(均匀分散)一致性 Hash(虚拟节点解决数据倾斜):
问题:物理节点少时,一致性 Hash 环分布不均
解决:每个物理节点映射 N 个虚拟节点(如 150 个)
物理节点 A → 虚拟节点 A#1(192.168.1.1#1), A#2, ..., A#150
物理节点 B → 虚拟节点 B#1, B#2, ..., B#150
优点:
- 数据分布更均匀
- 节点增减时受影响的数据更少面试追问
Q: Dubbo 的负载均衡在哪个层面实现?有哪些策略? A: Dubbo 是客户端负载均衡(类似 Ribbon),内置了:① Random LoadBalance(加权随机,默认);② RoundRobin LoadBalance(加权轮询);③ LeastActive LoadBalance(最少活跃调用);④ ConsistentHash LoadBalance(一致性 Hash)。
Q: 服务端负载均衡和客户端负载均衡能一起用吗? A: 可以,典型架构是 Nginx → 网关(Zuul/Gateway)→ Ribbon → 微服务。Nginx 做第一层(南北流量),Ribbon 做第二层(东西流量)。
常见错误
- ❌ 轮询 = 加权轮询(权重都是 1 时一样,但原理不同)
- ❌ 生产环境用 Ribbon 的纯随机策略不加权重——导致慢的机器分配同样多的请求
- ❌ 一致性 Hash 不配虚拟节点——物理节点少时数据严重倾斜
一句话总结
Nginx = 服务端负载均衡(集中式),Ribbon = 客户端负载均衡(从注册中心感知)。核心算法 = 加权轮询(Nginx SWRR)+ 一致性 Hash(虚拟节点解决倾斜)。
Q7: 如何设计一个分布式配置中心?⭐️⭐️
难度: ⭐️⭐️ | 考察点: 配置中心、Apollo、Nacos、配置热更新
面试官问:"你们项目的配置是怎么管理的?配置改了怎么热更新,不用重启服务?"
核心回答
配置中心的核心能力:
| 能力 | 描述 |
|---|---|
| 配置统一管理 | 多环境(dev/test/prod)、多应用、多集群的配置集中在一个平台上 |
| 配置热更新 | 修改配置后实时推送到各个服务节点,无需重启 |
| 灰度发布 | 先对部分节点推送新配置,验证无误后全量推送 |
| 权限与审计 | 谁在什么时间改了哪个配置,有日志可追溯 |
| 版本管理 | 配置回滚到历史版本 |
主流配置中心对比:
| 特性 | Apollo(携程) | Nacos(阿里) | Spring Cloud Config |
|---|---|---|---|
| 配置存储 | MySQL | MySQL / Derby | Git / SVN / 本地文件 |
| 推送方式 | HTTP 长轮询(1 秒) | HTTP 长轮询 | Git Webhook + Bus |
| 灰度发布 | ✅ 原生支持 | ✅ 支持 | ❌ 不支持 |
| 权限管理 | ✅ 强 | ✅ 中等 | ❌ 无 |
| 服务发现 | ❌ 不包含 | ✅ 包含 | ❌ 需配合 Eureka |
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
深度扩展
配置热更新的三种实现方式:
方式一:@RefreshScope(Spring Cloud)
@RefreshScope
@RestController
public class ConfigController {
@Value("${timeout}") private int timeout;
}
// 调用 /actuator/refresh 或 Spring Cloud Bus 广播 RefreshRemoteApplicationEvent
原理:创建代理 Bean,refresh 时销毁旧 Bean → 重新初始化新 Bean
方式二:@ConfigurationProperties(推荐)
@ConfigurationProperties(prefix = "app")
@Component
public class AppConfig {
private int timeout;
}
// 无需 @RefreshScope,@ConfigurationProperties 天然支持刷新
方式三:自行监听配置变更事件
configService.addListener("app.timeout", new ConfigChangeListener() {
public void onChange(ConfigChangeEvent event) {
// 更新内存中的配置值
}
});Apollo 推送机制(长轮询 + ReleaseMessage):
1. 客户端发起 HTTP 长轮询(timeout=90s)
2. Config Service 持有该请求(不立即返回)
3. 如果有配置变更 → 立即返回变更通知
4. 如果 90s 内无变更 → 返回 304 Not Modified
5. 客户端收到通知后 → 调用 Config Service 获取最新配置
6. 重复步骤 1
ReleaseMessage:配置变更时,Admin Service 往 MySQL ReleaseMessage 表插入一条记录
Config Service 定时(1s)扫描 ReleaseMessage 表,发现新消息就立即返回给长轮询的客户端面试追问
Q: 数据库连接池的配置热更新可行吗?为什么? A: 一般不可行——因为连接池在应用启动时就创建好了(final 连接数、final 超时参数)。强行热更新需要销毁旧连接池、创建新连接池(涉及大量连接的创建/销毁,风险极高)。这类基础设施配置通常建议重启生效。
Q: 配置中心挂了,服务会怎样? A: 客户端会有本地缓存(Apollo 默认缓存文件在 ~/opt/data/apollo/),配置中心宕机时从本地缓存读取。服务不会因为配置中心宕机而无法启动。
常见错误
- ❌ 敏感配置(密码、密钥)明文放配置中心——应该集成 Vault / KMS 加密存储
- ❌ 所有配置都做成热更新——数据库连接池、线程池等基础设施配置不适合热更新
- ❌
@Value注入的配置不配@RefreshScope——拿不到最新值
一句话总结
配置中心 = 配置统一管理 + 热更新 + 版本/权限。Apollo 功能最全(携程开源),Nacos = 配置 + 注册一体。热更新的实现是长轮询或 Spring Cloud Bus 广播。
Q8: RPC 和 HTTP 的区别?Dubbo 和 Spring Cloud 怎么选?⭐️⭐️
难度: ⭐️⭐️ | 考察点: RPC vs HTTP、Dubbo vs Spring Cloud
面试官问:"RPC 和 RESTful HTTP 有什么区别?Dubbo 和 Spring Cloud 各适合什么场景?"
核心回答
RPC vs HTTP:
| 对比维度 | RPC(如 Dubbo) | RESTful HTTP(如 Spring Cloud) |
|---|---|---|
| 传输协议 | 自定义协议(TCP),或 HTTP/2 | HTTP/1.1 → HTTP/2 |
| 序列化 | 高效(Hessian2、Protobuf、Kryo) | JSON / XML(文本,体积大) |
| 性能 | 高(二进制 + 长连接 + 多路复用) | 较低(文本 + 短连接/HTTP 头开销) |
| 服务治理 | 功能强大(注册、路由、负载均衡、限流) | 依赖 Spring Cloud 生态 |
| 跨语言 | 协议需单独实现(Dubbo 只支持 Java) | 天然支持(HTTP + JSON 通用) |
| 调试 | 困难(二进制协议) | 简单(curl、Postman 直接调试) |
| 适用场景 | 内部微服务间高性能调用 | 对外 API、跨语言调用、前后端 |
选型建议:
- 纯 Java 内部微服务 → Dubbo(性能 + 服务治理)
- 多语言 / 对外 API → Spring Cloud(生态 + 通用性)
- 两者兼需 → Dubbo + Spring Cloud 混合(Dubbo 注册到 Nacos,网关对外暴露 HTTP)
深度扩展
RPC 框架的核心组成:
Client 端:
[服务接口代理] → [序列化] → [协议编码] → [网络传输]
↓ TCP
Server 端: ↓
[服务实现] ← [反序列化] ← [协议解码] ← [网络接收]
核心模块:
1. 代理层(Proxy):透明化远程调用(像调本地方法一样)
2. 注册中心(Registry):服务注册与发现
3. 协议层(Protocol):自定义协议(Dubbo 协议、Triple 协议等)
4. 序列化层(Serializer):Hessian2、Protobuf、JSON
5. 传输层(Transport):Netty(TCP)、Tomcat(HTTP)
6. 集群层(Cluster):负载均衡、容错、路由Dubbo 调用过程:
Consumer 端: Provider 端:
接口代理(Proxy)
→ 集群容错(Cluster: Failover)
→ 负载均衡(LoadBalance: Random)
→ 过滤器链(Filter Chain)
→ 协议(Protocol: Dubbo)
→ 序列化(Serialization: Hessian2)
→ Netty 发送 ──TCP──→ Netty 接收
→ 反序列化
→ 协议解码
→ 过滤器链
→ 服务实现面试追问
Q: gRPC 和 Dubbo 有什么区别? A: gRPC 基于 HTTP/2 + Protobuf,天然跨语言(官方支持 11 种语言),Google 背书。Dubbo 基于自定义 TCP 协议,Java 生态服务治理更完善,阿里开源。gRPC 更适合多语言混合团队,Dubbo 更适合纯 Java 团队。
Q: 为什么说 Dubbo 的 Hessian2 比 JSON 快? A: ① Hessian2 是二进制协议,体积极小(不像 JSON 每个字段名都要传输);② Hessian2 有字段顺序映射(字段 1 永远是 int、字段 2 永远是 String),不需要解析字段名;③ 长连接 + 多路复用,省去了 HTTP 每次的三次握手开销。
常见错误
- ❌ 说 RPC 一定比 HTTP 快——HTTP/2 + Protobuf 的性能接近二进制 RPC,差距在缩小
- ❌ 对内部微服务和对外 API 用同一种协议——内部用 Dubbo,对外用 HTTP RESTful,各用其长
- ❌ RPC 不设超时——和 HTTP 一样,不设超时会拖死调用方线程池
一句话总结
RPC = 高性能(二进制 + 长连接 + 多路复用),适合内部微服务。HTTP = 通用(文本 + 语言无关),适合对外 API。Dubbo 是 Java RPC 的王者,Spring Cloud 是微服务生态全家桶。
Q9: 分布式锁的实现方案对比 ⭐️⭐️
难度: ⭐️⭐️ | 考察点: Redis、ZooKeeper、etcd 分布式锁
面试官问:"除了 Redis,分布式锁还能用哪些方案实现?各有什么优缺点?"
核心回答
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Redis | SET NX PX + Lua 解锁 + Redisson 看门狗 | 高性能、部署广泛 | 不保证强一致性(主从切换可能丢锁) |
| ZooKeeper | 临时顺序节点 + Watch 机制 | 强一致性、自动释放 | 性能不如 Redis、较重 |
| etcd | 租约(Lease)+ 事务(Txn) | 强一致性、有租约机制 | 部署不如 Redis 广泛 |
| 数据库 | 乐观锁(version)或 悲观锁(SELECT FOR UPDATE) | 简单、无需额外组件 | 性能差、有单点故障风险 |
选择建议:
- 性能优先、允许极低概率的锁失效 → Redis + Redisson
- 一致性优先、对安全性要求高 → ZooKeeper / etcd
- 系统简单、已有数据库没有 Redis/ZK → 数据库乐观锁
深度扩展
ZooKeeper 分布式锁原理:
1. 所有客户端在 /lock 下创建临时顺序节点
如:/lock/0000000001, /lock/0000000002, /lock/0000000003
2. 客户端查看 /lock 下所有子节点
- 如果自己是最小的节点号 → 获取锁成功
- 否则 → Watch 比它小一号的节点
3. 释放锁:删除自己的节点 → 触发下一个节点(等着锁的)的 Watch 回调
特性:
✅ 公平锁:按创建顺序排队
✅ 自动释放:客户端宕机 → 临时节点自动删除
✅ 强一致性:ZAB 协议保证
❌ 惊群效应:锁释放会通知下一个,但如果有 1000 个客户端排队,每次释放只通知下一个etcd 分布式锁:
// etcd 锁基于 Lease(租约) + 事务
// 1. 创建 Lease(带 TTL)
lease := clientv3.NewLease(client)
leaseResp, _ := lease.Grant(ctx, 30) // TTL 30s
// 2. 用事务尝试加锁(如果 key 不存在则 put)
txn := client.Txn(ctx).
If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)).
Then(clientv3.OpPut("lock", "holder", clientv3.WithLease(leaseResp.ID)))
resp, _ := txn.Commit()
// 3. 保持心跳:自动续约 KeepAlive
keepAliveCh, _ := lease.KeepAlive(ctx, leaseResp.ID) // 类似 Redisson 看门狗
// 优势:Lease 机制天然支持自动续期和释放面试追问
Q: ZooKeeper 的临时节点挂了是怎么释放的? A: ZK 客户端与服务端维护了 Session(会话),客户端宕机 → Session 超时(默认 sessionTimeout)→ ZK 服务端自动删除该客户端创建的所有临时节点 → 锁自动释放。
Q: Redis 的 Redlock 算法和 ZooKeeper 锁哪个更可靠? A: ZooKeeper 更可靠。Redlock 依赖系统时钟(存在时钟跳跃风险),ZooKeeper 基于 ZAB 协议提供强一致性。但代价是 ZK 性能远低于 Redis。学术界对 Redlock 的安全性也有争议(M. Kleppmann 的论文)。
常见错误
- ❌ 用数据库做分布式锁但忘记处理锁超时——持有锁的服务宕机,锁永久无法释放
- ❌ ZK 锁的客户端不处理 Session 过期——Session 过期后锁已释放,但客户端不知道,仍在执行"持有锁时"的逻辑
- ❌ Redis 锁的 value 使用固定字符串——无法区分锁的持有者,释放时误删他人的锁
一句话总结
Redis 锁 = 高性能但弱一致(SET NX PX + 看门狗)。ZooKeeper 锁 = 强一致 + 自动释放(临时顺序节点 + Watch)。etcd 锁 = 强一致 + 租约自动续期。性能优先选 Redis,一致性优先选 ZK/etcd。
Q10: 如何设计一个高可用的系统?⭐️⭐️
难度: ⭐️⭐️ | 考察点: 高可用架构设计、冗余、隔离、容灾
面试官问:"如果一个系统要求 99.99% 的可用性,你从哪些层面来设计?"
核心回答
高可用的核心原则:
高可用 = 冗余(Redundancy) + 故障转移(Failover) + 快速恢复(Recovery)分层设计:
| 层面 | 方案 | 解决的问题 |
|---|---|---|
| 接入层 | DNS 智能调度 + 多机房 | 单机房故障 |
| 网关层 | Nginx 多节点 + Keepalived | 网关单点故障 |
| 应用层 | 多副本 + 无状态 + 容器化 | 应用服务宕机 |
| 中间件层 | 主从/集群(Redis Cluster、Kafka 分区) | 中间件单点故障 |
| 数据层 | 主从同步 + 读写分离 + 异地灾备 | 数据库故障 |
| 运维层 | 监控 + 告警 + 自动扩容 + 应急预案 | 发现和恢复速度 |
可用性数字:
| 可用性 | 全年宕机时间 | 俗称 |
|---|---|---|
| 99% | 3.65 天 | 两个 9 |
| 99.9% | 8.76 小时 | 三个 9 |
| 99.99% | 52.56 分钟 | 四个 9 |
| 99.999% | 5.26 分钟 | 五个 9 |
深度扩展
应用层高可用设计:
1. 无状态设计:Session 外置(Redis)
→ 任何请求可被任何节点处理
→ 宕机了换一个节点就行
2. 冗余部署:至少 2 个副本,跨可用区
→ 一个可用区故障,另一个可用区接管
3. 健康检查 + 自动摘除:
→ 心跳失败 → 注册中心摘除 → 流量不再路由到故障节点
4. 优雅上下线:
- 上线:先注册但标记为"预热" → 流量逐步放大 → 标记为"正常"
- 下线:先标记为"不可用" → 等待处理完当前请求 → 关闭
5. 限流 + 熔断 + 降级(Sentinel)
→ 保证不被突发流量打垮数据层高可用设计:
MySQL 高可用:
主从 + 半同步复制(semi-sync) + MHA/Orchestrator 自动故障转移
异地灾备:binlog 跨机房同步 → 从库
Redis 高可用:
哨兵模式(中小规模) / Cluster 模式(大规模)
AOF + RDB 混合持久化
Kafka 高可用:
多分区 + 多副本(replication factor ≥ 3)
ISR(In-Sync Replica)机制保证数据不丢失多机房容灾架构:
同城双活(RPO 秒级,RTO 分钟级):
机房 A ←── 同步复制 ──→ 机房 B
DNS 分流(按地区/权重)
两地三中心:
生产机房 + 同城灾备 + 异地灾备
日常:生产 + 同城灾备承担流量
灾时:异地灾备接管
关键指标:
RPO(Recovery Point Objective):能接受多少数据丢失(目标:0)
RTO(Recovery Time Objective):多久能恢复服务(目标:分钟级)面试追问
Q: 99.99% 和 99.999% 的可用性设计有什么区别? A: 3 个 9 到 4 个 9 一般靠冗余 + 自动故障转移即可。4 个 9 到 5 个 9 需要更多:① 变更管理(大多数故障由变更引起);② 灰度/金丝雀发布;③ 在线热修复(不能重启);④ 混沌工程(主动注入故障,验证系统的容错能力)。
Q: "无状态"就一定高可用吗? A: 不是。无状态只解决了应用服务本身的可水平扩展问题,但依赖的下游(数据库、缓存、消息队列)如果出问题,整个系统仍然不可用。高可用是全链路的,每一层都需要设计。
常见错误
- ❌ 只考虑应用层高可用,忽略数据库高可用——数据库往往是单点瓶颈
- ❌ 认为"多部署几个节点 = 高可用"——还需要健康检查、自动摘除、故障转移等配套机制
- ❌ 把高可用等同于"不宕机"——高可用还包括"故障后多快能恢复"(MTTR)
一句话总结
高可用 = 冗余(消除单点) + 故障转移(自动切换) + 快速恢复(降 MTTR)。分层设计:接入层 → 网关 → 应用 → 中间件 → 数据层,每一层都要冗余 + 自动切换方案。