面试题背诵手册
基于「面试经验.md」整理的 12 大知识模块,每题配答案要点,便于背诵。
一、微服务
Q1. 什么是微服务架构?它和单体架构有什么区别?
答:
- 单体架构:所有功能模块打包在一个 WAR/JAR 中,共享一个数据库,部署在一个进程内。
- 微服务架构:按业务能力拆分为多个独立服务,每个服务独立部署、独立数据库、独立技术栈,通过 HTTP/RPC 通信。
- 核心区别:
维度 单体 微服务 部署 一次部署整体 独立部署 数据库 共享 每服务独立 技术栈 统一 可异构 扩展 整体扩展 按需扩展单个服务 故障影响 全局 局部隔离
Q2. 微服务核心组件有哪些?分别解决什么问题?
答:
| 组件 | 作用 | 代表实现 |
|---|---|---|
| 服务注册与发现 | 服务上下线自动感知 | Nacos、Eureka、Consul |
| 配置中心 | 集中管理配置,动态刷新 | Nacos Config、Apollo |
| 服务网关 | 统一入口、路由、鉴权、限流 | Spring Cloud Gateway、Zuul |
| 负载均衡 | 请求分发 | Ribbon、LoadBalancer |
| 服务调用 | 声明式 RPC | OpenFeign、Dubbo |
| 熔断降级 | 故障隔离、防止雪崩 | Sentinel、Hystrix |
| 分布式事务 | 跨服务数据一致性 | Seata |
| 链路追踪 | 全链路日志 | SkyWalking、Zipkin |
| 消息总线 | 配置广播 | Spring Cloud Bus |
Q3. 微服务拆分原则有哪些?
答:
- 单一职责原则(SRP):每个服务只负责一个业务领域。
- 领域驱动设计(DDD):按限界上下文(Bounded Context)拆分。
- 高内聚低耦合:服务内部功能紧密相关,服务间依赖最少。
- 数据库独占:每个服务拥有独立数据库,禁止跨库 Join。
- 合适的粒度:不要过细(导致分布式事务复杂),不要过粗(失去微服务意义)。
- 团队自治:一个服务对应一个小团队(康威定律)。
Q4. 微服务之间如何通信?各有什么优缺点?
答:
- HTTP/REST(OpenFeign):
- 优点:通用、跨语言、调试方便。
- 缺点:性能一般、序列化开销大。
- RPC(Dubbo、gRPC):
- 优点:性能高、二进制传输、长连接。
- 缺点:跨语言支持差、调试复杂。
- 消息队列(异步解耦):
- 优点:异步解耦、削峰填谷。
- 缺点:一致性要求高的场景不适用。
Q5. 微服务雪崩如何防止?熔断降级怎么实现?
答:
- 雪崩原因:服务 A 调用 B,B 响应慢导致 A 线程池耗尽,A 又被 C 调用,最终整个链路垮掉。
- 三大防线:
- 熔断(Circuit Breaker):错误率/慢调用达阈值后直接快速失败,类似保险丝。Sentinel 的熔断策略:慢调用比例、异常比例、异常数。
- 降级(Fallback):返回兜底数据(默认值、缓存、友好提示)。
- 限流:限制 QPS,超过阈值拒绝请求。Sentinel 的限流算法:快速失败、Warm Up、匀速排队。
- 舱壁隔离:线程池/信号量隔离,防止一个服务拖垮整个 Tomcat 线程池。
Q6. 服务注册与发现原理?Nacos 和 Eureka 区别?
答:
- 原理:服务启动时向注册中心注册(IP+端口),消费者从注册中心拉取服务列表并缓存本地,通过心跳维持。
- Nacos vs Eureka:
维度 Nacos Eureka CAP 支持 AP/CP 切换 仅 AP 健康检查 TCP/HTTP/自定义 心跳 配置中心 自带 需 Config Server 推送方式 UDP+定时拉取 定时拉取 实例上下线 推送通知 客户端拉取
二、Redis 备份
Q1. Redis 持久化有哪几种方式?区别是什么?
答:
| 维度 | RDB | AOF |
|---|---|---|
| 原理 | 定时快照,存储数据二进制 | 追加写命令日志 |
| 文件 | dump.rdb(紧凑) | appendonly.aof(较大) |
| 恢复速度 | 快 | 慢 |
| 数据安全 | 可能丢失最后一次快照后数据 | 最少丢 1 秒(everysec) |
| 触发方式 | save/bgsave/自动 | 实时追加 |
| 适用场景 | 备份、容灾 | 数据安全性要求高 |
Q2. RDB 的触发时机和 bgsave 原理?
答:
- 触发时机:
save:同步阻塞,生产禁用。bgsave:异步后台执行,主进程继续服务。- 配置自动触发:
save 900 1(900秒内1次修改)、save 300 10、save 60 10000。 - 主从全量同步时触发。
shutdown命令退出时触发。
- bgsave 原理:
- 主进程
fork()出子进程(利用 COW 写时复制技术)。 - 子进程遍历所有数据写入临时 RDB 文件。
- 写完后替换旧文件,通知主进程。
- fork 期间主进程阻塞,内存越大阻塞越久(这是 RDB 的缺点)。
- 主进程
Q3. AOF 的三种刷盘策略?怎么选?
答:
| 策略 | 含义 | 性能 | 安全 |
|---|---|---|---|
always | 每条命令都刷盘 | 最差 | 最高(不丢数据) |
everysec | 每秒刷盘一次 | 折中 | 较高(最多丢1秒) |
no | 交给 OS 决定 | 最好 | 最差 |
- 推荐:
everysec,性能和安全均衡,生产默认。
Q4. AOF 重写(Rewrite)是什么?为什么需要?
答:
- 问题:AOF 文件随命令追加不断变大,比如
incr key执行 100 次,AOF 记录 100 条,但实际只存了 1 个值。 - 重写:扫描当前内存数据,用最少的命令重新生成 AOF 文件,去除冗余。
- 触发:
- 自动:
auto-aof-rewrite-percentage 100(文件比上次重写后大一倍)、auto-aof-rewrite-min-size 64mb。 - 手动:
bgrewriteaof命令。
- 自动:
- 原理:fork 子进程,遍历内存生成新 AOF,期间新命令同时写入旧 AOF 和重写缓冲区,完成后合并。
Q5. Redis 4.0 混合持久化是什么?
答:
- 背景:RDB 恢复快但丢数据,AOF 数据全但恢复慢。
- 混合持久化:AOF 重写时,先以 RDB 格式写入当前内存快照,再以 AOF 格式追加重写期间的增量命令。
- 优点:兼顾恢复速度和数据完整性。
- 开启:
aof-use-rdb-preamble yes(4.0+ 默认开启)。
Q6. Redis 主从复制原理?
答:
- 全量同步(首次或断线太久):
- Slave 连接 Master,发送
PSYNC ? -1。 - Master 执行
bgsave生成 RDB,发送给 Slave。 - Master 同时把生成 RDB 期间的新命令写入复制缓冲区,发送给 Slave。
- Slave 加载 RDB 并执行缓冲区命令,完成同步。
- Slave 连接 Master,发送
- 增量同步(断线后重连):
- Slave 发送
PSYNC <replicationId> <offset>。 - Master 检查 offset 是否在复制积压缓冲区(默认 1MB)内。
- 在则只发送缺失部分;不在则全量同步。
- Slave 发送
- 命令传播:同步完成后,Master 每条写命令实时传播给 Slave。
Q7. Redis 哨兵(Sentinel)和集群(Cluster)区别?
答:
| 维度 | 哨兵 | 集群 |
|---|---|---|
| 架构 | 一主多从 + 哨兵监控节点 | 多主多从,分片存储 |
| 数据分片 | 不分片,全量复制 | 16384 槽位分片 |
| 故障转移 | 哨兵自动选主 | 节点间 Gossip 协议自动选主 |
| 容量瓶颈 | 单机内存上限 | 水平扩展 |
| 适用场景 | 数据量小、读多写少 | 数据量大、高并发 |
三、HashMap 加入/删除/扩容原理
Q1. HashMap 的底层数据结构?
答:
- JDK 1.7:数组 + 链表(头插法,并发可能形成环导致死循环)。
- JDK 1.8:数组 + 链表 + 红黑树(尾插法)。
- 链表长度 ≥ 8 且数组长度 ≥ 64:转红黑树。
- 红黑树节点 ≤ 6:退化为链表。
- 默认参数:
- 初始容量 16,负载因子 0.75,扩容阈值 = 容量 × 0.75。
- 树化阈值 8,退化阈值 6,树化最小数组容量 64。
Q2. HashMap 的 put 流程?
答:
- 计算 key 的 hash:
hash = (h = key.hashCode()) ^ (h >>> 16)(扰动,让高位也参与运算,减少冲突)。 - 定位桶下标:
index = (n - 1) & hash(n 是数组长度,必为 2 的幂次,位运算等价取模但更快)。 - 桶为空:直接放入。
- 桶非空:
- 第一个节点 key 相等:覆盖 value。
- 是树节点:调用红黑树插入。
- 是链表:尾插法遍历,找到则覆盖,找不到则尾插。插入后长度 ≥ 8 尝试树化。
++size > threshold:扩容。
Q3. HashMap 的扩容机制?
答:
- 触发条件:
size > capacity × loadFactor(默认 16 × 0.75 = 12)。 - 扩容过程:
- 创建新数组,容量翻倍(
newCap = oldCap << 1)。 - 遍历旧数组每个桶:
- 单节点:重新计算下标
newIndex = (newCap - 1) & hash,由于容量翻倍,新位置要么是原位置,要么是「原位置 + 旧容量」。 - 链表:JDK 1.8 拆分为「低位链」和「高位链」两条,分别放到新数组的原位置和原位置+旧容量。
- 红黑树:拆分后若节点 ≤ 6 退化为链表。
- 单节点:重新计算下标
- 创建新数组,容量翻倍(
- 为什么容量是 2 的幂次:
index = (n-1) & hash位运算代替取模,性能高。- 扩容时只需判断 hash 的新增高位是 0 还是 1,决定是否迁移。
Q4. HashMap 的 get 流程?
答:
- 计算 key 的 hash。
- 定位桶
index = (n - 1) & hash。 - 桶为空:返回 null。
- 第一个节点 key 相等:返回 value。
- 是树节点:红黑树查找 O(logN)。
- 是链表:遍历查找 O(N)。
Q5. HashMap 为什么线程不安全?
答:
- JDK 1.7:并发 put 头插法导致链表成环,get 时死循环。
- JDK 1.8:
- 并发 put 可能数据覆盖(两个线程同时判断桶为空,一个写入被覆盖)。
- size 计数不准。
- 扩容时数据丢失。
- 替代方案:
ConcurrentHashMap(推荐):JDK 1.8 用 CAS + synchronized 锁单桶,并发安全且性能好。Collections.synchronizedMap:全表锁,性能差。
Q6. HashMap 的 hash 为什么要扰动?
答:
hashCode()是 int(32位),如果直接index = hash % n,只有低位参与运算,冲突率高。- 扰动
(h = hashCode) ^ (h >>> 16)让高 16 位和低 16 位异或,使高位特性也影响下标,分布更均匀,减少冲突。
四、JVM
Q1. JVM 内存区域划分?哪些是线程私有?哪些共享?
答:
| 区域 | 线程私有 | 作用 | 是否会 OOM |
|---|---|---|---|
| 程序计数器 | 是 | 记录当前执行字节码行号 | 不会 |
| 虚拟机栈 | 是 | 方法栈帧(局部变量、操作数栈) | 是(StackOverflowError / OOM) |
| 本地方法栈 | 是 | Native 方法栈帧 | 是 |
| 堆 | 否 | 对象实例、数组 | 是 |
| 方法区 | 否 | 类信息、常量、静态变量 | 是 |
| 运行时常量池 | 否 | 方法区内,字面量、符号引用 | 是 |
| 直接内存 | 否 | NIO 的 DirectByteBuffer | 是 |
Q2. 对象创建过程?
答:
- 类加载检查:常量池定位符号引用,检查是否已加载。
- 分配内存:
- 指针碰撞:内存规整(Serial、ParNew)。
- 空闲列表:内存不规整(CMS)。
- 并发安全:CAS + 失败重试,或 TLAB(每个线程预分配一块)。
- 初始化零值:成员变量赋默认值(0、null)。
- 设置对象头:Mark Word(hash、年龄、锁状态)、类型指针、数组长度。
- 执行
<init>:构造函数赋初值。
Q3. 对象的内存布局?
答:
- 对象头:
- Mark Word:hash、GC 年龄、锁标志位、偏向线程 ID。
- 类型指针:指向类元数据。
- 数组长度(数组对象才有)。
- 实例数据:字段值(相同宽度的字段放一起,父类在前)。
- 对齐填充:8 字节对齐。
Q4. 类加载过程?双亲委派模型?
答:
- 加载→验证→准备→解析→初始化→使用→卸载:
- 加载:通过类全限定名获取二进制字节流,生成 Class 对象。
- 验证:文件格式、元数据、字节码、符号引用。
- 准备:静态变量赋默认值(int=0)。
- 解析:符号引用替换为直接引用。
- 初始化:执行
<clinit>,静态变量赋初值、静态代码块。
- 双亲委派:类加载时先委托父加载器加载,父加载不了再自己加载。
- 启动类加载器(Bootstrap):
rt.jar。 - 扩展类加载器(Extension):
ext/*.jar。 - 应用类加载器(App):classpath。
- 好处:防止核心类被篡改(自定义
java.lang.String会被 Bootstrap 加载),避免重复加载。 - 打破场景:SPI(JDBC)、Tomcat(每个 Web 应用独立 ClassLoader)、OSGi。
- 启动类加载器(Bootstrap):
Q5. GC 判断对象存活的方法?
答:
- 引用计数法:每被引用+1,引用失效-1。问题:循环引用(A 引 B、B 引 A)导致永远不死。早期 Python 用,JVM 不用。
- 可达性分析:从 GC Roots 出发,能到达的对象存活,否则回收。
- GC Roots:
- 虚拟机栈中的局部变量。
- 方法区的静态变量。
- 方法区的常量。
- 本地方法栈 JNI 引用。
- 活跃线程。
- GC Roots:
Q6. 四种引用类型?
答:
| 引用 | 特性 | 应用 |
|---|---|---|
| 强引用 | Object o = new Object(),永不回收 | 普通引用 |
| 软引用 | 内存不足时回收 | 缓存(SoftReference) |
| 弱引用 | 下次 GC 必回收 | ThreadLocal、WeakHashMap |
| 虚引用 | 形同虚设,仅跟踪回收事件 | 跟踪对象被 GC 的时机 |
Q7. 垃圾收集算法?
答:
- 标记-清除:标记存活对象,清除其他。缺点:内存碎片。
- 标记-复制:将存活对象复制到另一半,清空原区。用于新生代(Eden:S0:S1 = 8:1:1)。
- 标记-整理:标记存活对象,向一端移动整理。用于老年代。
- 分代收集:
- 新生代:复制算法(对象朝生夕死,存活少)。
- 老年代:标记-清除/整理(存活多,不适合复制)。
Q8. 常见垃圾收集器?
答:
| 收集器 | 作用域 | 算法 | 特点 |
|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程,STW,客户端 |
| ParNew | 新生代 | 复制 | Serial 多线程版,配合 CMS |
| Parallel Scavenge | 新生代 | 复制 | 吞吐量优先 |
| Serial Old | 老年代 | 标记整理 | 单线程 |
| Parallel Old | 老年代 | 标记整理 | 多线程,吞吐量 |
| CMS | 老年代 | 标记清除 | 低延迟,4阶段:初始标记→并发标记→重新标记→并发清除 |
| G1 | 整堆 | Region化标记整理 | 可预测停顿,大堆 |
| ZGC | 整堆 | Region + 染色指针 | <10ms 停顿,TB 级堆 |
Q9. JVM 调优常见参数?
答:
- 堆:
-Xms(初始堆)、-Xmx(最大堆)、-Xmn(新生代)。 - 元空间:
-XX:MetaspaceSize、-XX:MaxMetaspaceSize。 - 栈:
-Xss(线程栈大小,默认 512K~1M)。 - GC:
-XX:+UseG1GC、-XX:MaxGCPauseMillis=200。 - GC 日志:
-Xloggc:、-XX:+PrintGCDetails。 - OOM 时 dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=。
五、xxl-job 多实例启动问题
Q1. xxl-job 是什么?核心架构?
答:
- 分布式任务调度平台:调度中心(Admin)+ 执行器(业务系统集成 SDK)。
- 架构:
- 调度中心:负责任务调度、日志管理、配置管理。
- 执行器:业务系统嵌入 SDK,接收调度并执行任务。
- 通信:HTTP。
- 特点:动态路由、失败重试、告警、任务分片、日志可视化。
Q2. 多个 jar 包部署会有多个 xxl-job 执行器同时启动,如何解决?
答:
- 问题场景:微服务多实例部署时,每个实例都注册成执行器,同一个任务可能被多个实例同时执行,导致重复执行。
- 解决方案:
方案一:保证执行器 appname + 注册地址唯一
- 每个实例注册时使用唯一地址(IP + 不同端口):yaml
xxl: job: executor: appname: xxx-job address: # 留空则自动获取 ip: # 留空则自动获取 port: 9999 # 每个实例不同端口 - 调度中心看到的是多个执行器实例,任务按路由策略分发。
方案二:合理配置路由策略
| 路由策略 | 说明 |
|---|---|
| FIRST / LAST | 第一个/最后一个,单机执行 |
| ROUND | 轮询,多机分担 |
| RANDOM | 随机 |
| CONSISTENT_HASH | 一致性哈希 |
| LEAST_FREQUENTLY_USED | 最不经常使用 |
| LEAST_RECENTLY_USED | 最近最久未使用 |
| FAILOVER | 故障转移,顺序心跳检测,跳过故障机 |
| BUSYOVER | 忙碌转移 |
| SHARDING_BROADCAST | 分片广播,所有实例都执行(适合大数据分片) |
- 单实例执行:用
FIRST或FAILOVER。 - 分布式处理:用
SHARDING_BROADCAST,每个实例处理一部分分片。
方案三:分布式锁保证幂等
- 任务逻辑入口加 Redis 分布式锁:java
@XxlJob("demoJobHandler") public void execute() { String lockKey = "job:demoJob:lock"; String requestId = UUID.randomUUID().toString(); try { // 尝试加锁,过期时间略大于任务执行时间 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.MINUTES); if (!Boolean.TRUE.equals(locked)) { XxlJobHelper.log("未获取到锁,跳过执行"); return; } // 执行业务逻辑 doBusiness(); } finally { // 释放锁需校验 requestId,防止误删 releaseLock(lockKey, requestId); } }
方案四:保证业务幂等
- 即使重复执行也无影响,例如:状态机判断、唯一索引、
insert ignore、update ... where status=xxx。
Q3. xxl-job 路由策略「分片广播」怎么用?
答:
- 所有执行器实例都会被调度,每个实例通过
XxlJobHelper.getShardIndex()和getShardTotal()获取自己处理的分片。 - 示例:java
@XxlJob("shardingJob") public void shardingJob() { int index = XxlJobHelper.getShardIndex(); // 当前实例编号 int total = XxlJobHelper.getShardTotal(); // 总实例数 // 处理 id % total == index 的数据 List<Long> ids = mapper.selectByShard(index, total); process(ids); } - 适合大数据量处理(如百万用户推送、对账)。
六、MQ 消息消费
Q1. MQ 消费消息的两种模式?
答:
- Pull(拉)模式:消费者主动从 Broker 拉取消息。
- 优点:消费速率可控。
- 缺点:实时性差,需轮询。
- RocketMQ 默认 Pull。
- Push(推)模式:Broker 主动推给消费者。
- 优点:实时性好。
- 缺点:消费者处理慢会堆积。
- RabbitMQ 是 Push(长轮询实现)。
Q2. 如何保证消息不丢失?
答: 消息从生产到消费经过三环节:生产者→Broker→消费者,每环都可能丢。
| 环节 | 丢失原因 | 解决方案 |
|---|---|---|
| 生产者→Broker | 网络异常、Broker 宕机 | 1. 同步发送 + 重试 2. Confirm 机制(RabbitMQ)/ ACK(RocketMQ) 3. 本地消息表 + 定时补偿 |
| Broker 存储 | 宕机内存丢失 | 1. 持久化(磁盘) 2. 主从复制 3. 镜像队列(RabbitMQ)/ 同步刷盘(RocketMQ) |
| Broker→消费者 | 消费者处理失败但已 ACK | 1. 手动 ACK,处理成功后才确认 2. 失败重试 + 死信队列 |
Q3. 如何保证消息顺序性?
答:
- 全局有序:单队列单消费者,性能差,极少用。
- 局部有序(推荐):同一业务 ID 的消息发到同一队列,由同一消费者消费。
- 实现:
- RabbitMQ:单队列单消费者,或一致性哈希交换器。
- RocketMQ:
MessageQueueSelector按orderId % 队列数选择队列,消费端MessageListenerOrderly单线程消费同一队列。 - Kafka:同一 key 的消息进入同一分区,分区内有序。
Q4. 如何保证消息幂等(避免重复消费)?
答:
- 原因:网络抖动导致 ACK 丢失,Broker 重发;消费者重启。
- 方案:
- 唯一 ID + 去重表:业务消息表,消费前检查是否已处理。
- Redis setnx:
setnx(msgId, 1, expire),成功才处理。 - 状态机:业务状态只能前进,
update ... where status='待支付'。 - 数据库唯一索引:业务表唯一键约束。
- 乐观锁:
version字段。
Q5. 如何处理消息积压?
答:
- 临时扩容:
- 增加消费者数量(注意 Kafka 消费者数不能超过分区数)。
- 临时 Topic 转发:消费后不处理,直接转发到一个新 Topic(分区数扩大 10 倍),再用更多消费者处理。
- 优化消费逻辑:
- 批量处理(一次拉多条)。
- 异步化:消费后丢线程池异步处理,快速 ACK。
- 跳过堆积消息:先丢弃非关键消息(埋点日志)。
- 根因排查:消费者是否慢 SQL?外部依赖超时?线程池满?
Q6. 死信队列(DLX/DLQ)是什么?什么时候用?
答:
- 死信:被拒绝(nack/reject 且 requeue=false)、TTL 过期、队列满的消息。
- 应用场景:
- 消费失败重试 N 次仍失败,转入 DLQ 人工处理。
- 延迟队列:发到 TTL 队列,过期后进入死信目标队列,实现延迟消费。
- RabbitMQ 配置:java
Map<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange", "dlx.exchange"); args.put("x-dead-letter-routing-key", "dlx.routing.key"); args.put("x-message-ttl", 60000); channel.queueDeclare("biz.queue", true, false, false, args);
Q7. RocketMQ 和 Kafka 和 RabbitMQ 区别?怎么选型?
答:
| 维度 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|
| 语言 | Scala/Java | Java | Erlang |
| 吞吐量 | 百万级 | 十万级 | 万级 |
| 延迟 | ms | ms | μs |
| 顺序 | 分区有序 | 分区有序 | 队列有序 |
| 事务 | 弱 | 强(事务消息) | 弱 |
| 延迟消息 | 不支持 | 支持(18个级别) | 插件/TTL+DLX |
| 应用场景 | 日志、大数据 | 金融、电商订单 | 业务消息、复杂路由 |
七、负载均衡算法
Q1. 常见负载均衡算法有哪些?
答:
- 轮询(Round Robin):1→2→3→1→2→3,按顺序循环。
- 加权轮询(Weighted Round Robin):按权重比例分配,性能差异大时使用。如 A:B:C = 5:2:1。
- 随机(Random):随机选一个,简单。
- 加权随机(Weighted Random):按权重概率随机。
- 源地址哈希(IP Hash):相同 IP 固定到同一服务器,可保持 Session。
- 最小连接数(Least Connections):选当前连接数最少的,适合长连接。
- 一致性哈希(Consistent Hash):环状哈希空间,节点增减只影响相邻段,减少迁移。
- 最快响应(Fastest Response):选响应时间最短的。
Q2. 一致性哈希原理?为什么需要?
答:
- 问题:普通哈希
hash % N,节点数 N 变化时几乎全部数据要迁移。 - 一致性哈希:
- 将整个哈希空间组织成环(0 ~ 2^32 - 1)。
- 节点哈希后落到环上某点。
- 数据顺时针找到的第一个节点就是它的归属。
- 节点增减只影响环上相邻段的数据。
- 虚拟节点:解决数据倾斜(节点少时分布不均),每个物理节点对应 150+ 虚拟节点。
- 应用:Redis Cluster 的槽位、Nginx 一致性哈希、Dubbo 路由。
Q3. 轮询和加权轮询的实现?Nginx 加权轮询算法?
答:
- Nginx 加权轮询算法(平滑加权轮询):
- 每个节点有
当前权重 currentWeight和有效权重 effectiveWeight。 - 每次选择时:所有
currentWeight += effectiveWeight,选最大currentWeight的节点,将其currentWeight -= 总权重。 - 效果:权重高的节点被分散选择,不会集中命中。
- 示例(A=5, B=1, C=1):
初始 [0,0,0] 1. [5,1,1] → 选A → [-2,1,1] 2. [3,2,2] → 选A → [-4,2,2] 3. [1,3,3] → 选B → [1,-4,3] ...
- 每个节点有
Q4. Ribbon 的负载均衡策略有哪些?
答:
| 策略 | 类 | 说明 |
|---|---|---|
| 轮询(默认) | RoundRobinRule | 默认策略 |
| 随机 | RandomRule | 随机选 |
| 重试 | RetryRule | 轮询+失败重试 |
| 最佳可用 | BestAvailableRule | 选并发最小的 |
| 可用过滤 | AvailabilityFilteringRule | 过滤故障,再选 |
| 响应时间加权 | WeightedResponseTimeRule | 按响应时间分配权重 |
| 区域感知 | ZoneAvoidanceRule | AWS 场景,避开故障区域 |
| 一致性哈希 | — | 需自定义 |
Q5. Dubbo 的负载均衡策略?
答:
| 策略 | 说明 |
|---|---|
| Random(默认) | 按权重随机 |
| RoundRobin | 加权轮询 |
| LeastActive | 最少活跃调用数,慢的少分配 |
| ConsistentHash | 一致性哈希,相同参数请求同一提供者 |
| ShortestResponse | 最短响应时间(2.7+) |
八、Docker 与 K8s
Q1. Docker 是什么?核心概念?
答:
- Docker:基于 Linux 内核 namespace(隔离)和 cgroup(资源限制)的容器化技术,相比虚拟机更轻量。
- 核心概念:
概念 说明 镜像(Image) 只读模板,分层存储 容器(Container) 镜像运行实例,可读写层 仓库(Registry) 镜像存储分发,Docker Hub、Harbor Dockerfile 镜像构建脚本 数据卷(Volume) 持久化数据 网络(Network) bridge/host/overlay
Q2. Docker 和虚拟机区别?
答:
| 维度 | 虚拟机 | Docker |
|---|---|---|
| 隔离级别 | 操作系统级 | 进程级 |
| 资源占用 | GB 级 | MB 级 |
| 启动 | 分钟 | 秒 |
| 镜像大小 | GB | MB |
| 性能 | 弱于原生 | 接近原生 |
| 系统支持 | 每个VM独立OS | 共享宿主机内核 |
Q3. Dockerfile 常用指令?
答:
dockerfile
FROM openjdk:17 # 基础镜像
WORKDIR /app # 工作目录
COPY target/app.jar app.jar # 拷贝
EXPOSE 8080 # 声明端口
ENV TZ=Asia/Shanghai # 环境变量
ENTRYPOINT ["java","-jar","app.jar"] # 启动命令- CMD vs ENTRYPOINT:
- CMD:容器启动默认命令,可被
docker run后参数覆盖。 - ENTRYPOINT:启动命令,参数追加到后面,不易覆盖。
- 推荐组合:ENTRYPOINT + CMD(CMD 作为 ENTRYPOINT 的默认参数)。
- CMD:容器启动默认命令,可被
Q4. Docker 网络模式有哪些?
答:
| 模式 | 说明 | 应用 |
|---|---|---|
| bridge(默认) | 桥接 docker0,每容器独立 IP | 大多数场景 |
| host | 共享宿主机网络栈,无独立 IP | 性能敏感 |
| none | 无网络 | 安全隔离 |
| container | 共享另一容器网络 | sidecar |
| overlay | 跨主机通信 | K8s、Swarm |
Q5. K8s 是什么?核心组件?
答:
- Kubernetes:容器编排系统,负责容器部署、扩缩容、故障恢复、负载均衡。
- Master 节点:
组件 作用 kube-apiserver API 入口,所有操作都经过它 etcd 分布式 KV 存储,保存集群状态 kube-scheduler 调度 Pod 到合适 Node kube-controller-manager 控制器集合(副本、节点、端点等) - Worker 节点:
组件 作用 kubelet 管理 Pod 生命周期,向 Master 汇报 kube-proxy 维护网络规则,实现 Service 负载均衡 容器运行时 Docker / containerd
Q6. Pod 是什么?为什么 K8s 用 Pod 而不直接用容器?
答:
- Pod:K8s 最小调度单位,包含一个或多个共享网络和存储的容器。
- 为何用 Pod:
- 容器间共享网络(同 localhost)、共享 Volume。
- 适合 sidecar 模式(如主业务容器 + 日志收集容器)。
- 解耦:Pod 内多容器同生命周期。
- 一个 Pod 一个 IP,Pod 内容器共享。
Q7. K8s 常见工作负载?
答:
| 类型 | 说明 | 场景 |
|---|---|---|
| Deployment | 无状态应用,管理 ReplicaSet | Web 服务 |
| StatefulSet | 有状态,稳定网络标识 + 持久存储 | 数据库、MQ |
| DaemonSet | 每个 Node 跑一个 | 日志采集、监控 |
| Job / CronJob | 一次性 / 定时任务 | 批处理、xxl-job |
| Service | 服务抽象 + 负载均衡 | 对外暴露 |
Q8. Service 类型有哪些?
答:
| 类型 | 说明 |
|---|---|
| ClusterIP | 集群内访问(默认) |
| NodePort | 节点端口暴露,范围 30000-32767 |
| LoadBalancer | 云厂商 LB |
| ExternalName | CNAME 到外部域名 |
Q9. 滚动更新和回滚原理?
答:
- 滚动更新:Deployment 默认策略,新建 RS,逐步增加新版本 Pod,减少旧版本 Pod,零停机。
- 回滚:
kubectl rollout undo deployment/xxx --to-revision=N,恢复到历史 ReplicaSet。 - 控制参数:
maxSurge:滚动时最多超出期望副本数(默认 25%)。maxUnavailable:滚动时最多不可用副本数(默认 25%)。
九、MySQL 索引与三大日志
Q1. 索引数据结构?为什么用 B+ 树?
答:
- InnoDB 索引:B+ 树。
- 为什么 B+ 树:
- 对比二叉树:数据量大时树太高,磁盘 IO 多。
- 对比 B 树:B+ 树非叶子节点不存数据,扇出更大,树更矮;叶子节点用链表连接,范围查询高效。
- 对比红黑树:同样树高问题。
- 对比 Hash:Hash 不支持范围查询、排序。
- 特性:
- 叶子节点存数据,非叶子节点存索引。
- 叶子节点用双向链表连接。
- 一个节点 = 一页(16KB),3~4 层可存千万级数据。
Q2. 聚簇索引和非聚簇索引区别?
答:
| 维度 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 数据存储 | 叶子节点存整行数据 | 叶子节点存主键值 |
| 数量 | 每表一个(主键) | 多个 |
| 查询 | 直接返回数据 | 需回表查询主键索引 |
| InnoDB | 主键即聚簇 | 唯一/普通索引为二级 |
- 回表:二级索引查到主键,再到聚簇索引查整行数据。
- 覆盖索引:查询字段都在二级索引中,无需回表。
explain中 Extra 显示Using index。
Q3. 索引失效场景有哪些?
答:
- 不满足最左前缀:联合索引
(a,b,c),查询条件无 a 或跳过 a。 - 索引列做运算/函数:
WHERE age + 1 = 20、WHERE YEAR(create_time)。 - 类型隐式转换:字符串列
WHERE phone = 13800000000(数字转字符串)。 !=、<>、NOT IN、NOT LIKE:可能全表扫描。LIKE以%开头:LIKE '%abc',LIKE 'abc%'可用索引。OR两边不都有索引:一边无索引则全表。ORDER BY字段不在索引中或顺序不一致。- 优化器认为全表扫描更快:数据量小或区分度低(如性别)。
IS NULL/IS NOT NULL:可能不走索引。
Q4. redo log 是什么?
答:
- 作用:保证事务的持久性(D),崩溃恢复后数据不丢。
- 原理:InnoDB 引擎层日志,记录「数据页的物理修改」。事务提交时先写 redo log(顺序写,性能高),再异步刷数据页到磁盘(随机写)。
- WAL(Write-Ahead Log):先写日志,再写数据。崩溃后通过 redo log 恢复。
- 刷盘策略(
innodb_flush_log_at_trx_commit):- 0:每秒刷盘,崩溃丢 1 秒数据。
- 1:每次提交都刷盘(默认,最安全)。
- 2:提交时写到 OS Buffer,每秒 fsync。
- 大小:固定大小循环写,写满后触发刷盘。
Q5. bin log 是什么?
答:
- 作用:MySQL Server 层日志,用于复制和数据恢复。
- 格式:
格式 内容 优缺点 Statement SQL 语句 日志小,但函数/now() 不一致 Row 行变更(前像+后像) 准确,但日志大 Mixed 混合 折中 - 与 redo log 区别:
维度 redo log bin log 层级 InnoDB 引擎 Server 层 内容 物理日志(页修改) 逻辑日志(SQL/行变更) 写入 循环写 追加写 用途 崩溃恢复 复制、备份恢复 事务 提交时已写 提交后才写
Q6. undo log 是什么?
答:
- 作用:保证事务的原子性(A)和实现 MVCC。
- 原理:记录数据修改前的旧值(前像)。
- 事务回滚:用 undo log 恢复数据。
- MVCC:旧版本数据通过 undo log 链找到,供快照读。
- 类型:
- insert undo log:insert 产生的,事务提交后即删除。
- update undo log:update/delete 产生的,用于 MVCC,无事务依赖后由 Purge 线程清理。
Q7. 两阶段提交(redo log + bin log)?
答:
- 目的:保证 redo log 和 bin log 一致,避免主从数据不一致。
- 流程:
- Prepare:写 redo log,标记为 prepare 状态。
- 写 bin log。
- Commit:写 redo log,标记为 commit 状态。
- 崩溃恢复:
- redo log 已 commit:直接提交。
- redo log 是 prepare:
- 检查 bin log 是否完整:完整则提交,不完整则回滚。
Q8. MVCC 原理?
答:
- 目的:多版本并发控制,提升并发性能,解决读写冲突。
- 组成:
- 隐藏字段:
DB_TRX_ID(事务 ID)、DB_ROLL_PTR(指向 undo log 的指针)。 - undo log 版本链:每次修改通过 undo log 形成历史版本链。
- ReadView:记录「活跃事务列表」,决定哪个版本可见。
- 隐藏字段:
- 可见性判断(RC 每次快照读生成新 ReadView,RR 只在第一次生成):
- 版本的 trx_id < min_trx_id:可见。
- 版本的 trx_id > max_trx_id:不可见。
- 在活跃列表中:不可见。
- 否则可见。
- RC vs RR:
- RC(读已提交):每次 SELECT 都生成新 ReadView,能看到最新提交。
- RR(可重复读):事务第一次 SELECT 时生成 ReadView,后续复用,保证可重复读。
十、设计模式
Q1. 单例模式(重点)
答:
- 定义:确保一个类只有一个实例,并提供全局访问点。
- 应用场景:配置管理、线程池、日志对象、Spring Bean(默认单例)。
饿汉式(线程安全)
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}- 类加载时创建,线程安全,但可能浪费内存。
懒汉式(双重检查,推荐)
java
public class Singleton {
// volatile 防止指令重排序(new 不是原子操作)
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免无谓加锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,防止重复创建
instance = new Singleton();
}
}
}
return instance;
}
}- 为什么要 volatile:
new Singleton()实际是三步:分配内存→初始化→赋值引用。指令重排序可能变为「分配→赋值→初始化」,其他线程拿到未初始化对象。
静态内部类(推荐)
java
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() { return Holder.INSTANCE; }
}- 利用类加载机制保证线程安全,懒加载。
枚举(最佳,防反射破坏)
java
public enum Singleton {
INSTANCE;
public void method() {}
}- 天然防反射、防序列化破坏。
Q2. 单例模式被反射破坏怎么办?序列化破坏怎么办?
答:
- 反射破坏:通过
setAccessible(true)调用私有构造器创建新实例。- 防御:构造器中判断实例是否已存在,存在则抛异常。
- 枚举单例:JVM 保证枚举单例,反射无法创建。
- 序列化破坏:反序列化时创建新对象。
- 防御:实现
readResolve()方法返回实例。 - 枚举单例:序列化机制保证枚举单例。
- 防御:实现
Q3. 工厂模式
答:
- 定义:定义创建对象的接口,让子类决定实例化哪个类。
- 应用场景:
- 对象创建逻辑复杂。
- 客户端不关心创建细节。
- 需要解耦创建和使用。
- 三种形态:
- 简单工厂:一个工厂类通过
if/else创建不同产品。违反开闭原则。 - 工厂方法:每个产品对应一个工厂类,扩展新产品时新增工厂。类多。
- 抽象工厂:工厂创建一系列相关产品(产品族)。
- 简单工厂:一个工厂类通过
- 实战案例:java
// 支付方式工厂 public interface PayFactory { Pay createPay(); } public class AliPayFactory implements PayFactory { public Pay createPay() { return new AliPay(); } } public class WeChatPayFactory implements PayFactory { public Pay createPay() { return new WeChatPay(); } } // 使用 PayFactory factory = new AliPayFactory(); Pay pay = factory.createPay(); pay.pay(100); - Spring 中的工厂:
BeanFactory创建 Bean,FactoryBean自定义创建逻辑。
Q4. 策略模式
答:
- 定义:定义一系列算法,封装起来,可互相替换。
- 应用场景:
- 多种支付方式、多种折扣算法、多种排序方式。
- 消除大量
if/else。
- 实战案例:java
// 策略接口 public interface DiscountStrategy { BigDecimal discount(BigDecimal price); } // 满减 public class FullReductionStrategy implements DiscountStrategy { public BigDecimal discount(BigDecimal price) { return price.compareTo(new BigDecimal("100")) >= 0 ? price.subtract(new BigDecimal("20")) : price; } } // 打折 public class PercentageStrategy implements DiscountStrategy { private BigDecimal rate; public PercentageStrategy(BigDecimal rate) { this.rate = rate; } public BigDecimal discount(BigDecimal price) { return price.multiply(rate); } } // 上下文 public class DiscountContext { private DiscountStrategy strategy; public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; } public BigDecimal calc(BigDecimal price) { return strategy.discount(price); } } - 结合 Spring:将策略 Bean 注入 Map,按 key 选择:java
@Component public class StrategyFactory { @Autowired private Map<String, DiscountStrategy> strategyMap; public DiscountStrategy get(String type) { return strategyMap.get(type); } }
Q5. 策略模式和工厂模式区别?
答:
| 维度 | 策略模式 | 工厂模式 |
|---|---|---|
| 关注点 | 行为/算法 | 对象创建 |
| 客户端 | 知道有多种策略,主动选择 | 不关心创建细节 |
| 目的 | 运行时切换行为 | 解耦创建和使用 |
| 应用 | 折扣、支付、排序 | 实例化对象 |
Q6. 开闭原则(OCP)?设计模式如何体现?
答:
- 开闭原则:对扩展开放,对修改关闭。新增功能时添加新代码,不改老代码。
- 体现:
- 工厂模式:新增产品 → 新增工厂类,不改老工厂。
- 策略模式:新增策略 → 新增策略类,不改上下文。
- 单例模式:本身不体现,但单例避免重复创建。
十一、MySQL 事务与 Spring 事务
Q1. 事务的 ACID 特性?
答:
| 特性 | 含义 | 由谁保证 |
|---|---|---|
| 原子性 Atomicity | 全部成功或全部回滚 | undo log |
| 一致性 Consistency | 数据合法状态 | 业务 + AID |
| 隔离性 Isolation | 并发事务互不干扰 | 锁 + MVCC |
| 持久性 Durability | 提交后永久保存 | redo log |
Q2. MySQL 事务隔离级别?各级别解决什么问题?
答:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(RU) | ✓ 可能 | ✓ 可能 | ✓ 可能 |
| 读已提交(RC) | × | ✓ 可能 | ✓ 可能 |
| 可重复读(RR,默认) | × | × | ✓ 可能(InnoDB 用间隙锁解决) |
| 串行化(Serializable) | × | × | × |
- 脏读:读到未提交的数据。
- 不可重复读:同一事务两次读同一行,结果不同(被 update)。
- 幻读:同一事务两次查询,行数不同(被 insert/delete)。
- InnoDB 在 RR 级别:通过 MVCC(快照读)+ 间隙锁(当前读)解决幻读。
Q3. MySQL 的锁有哪些?
答:
- 按粒度:
锁 粒度 冲突 表锁 表级 锁整表,并发差 行锁 行级 锁索引项,并发好 页锁 页级 介于两者 - 按类型:
锁 含义 共享锁(S锁,读锁) LOCK IN SHARE MODE,可读不可写排他锁(X锁,写锁) FOR UPDATE,独占 - InnoDB 行锁:
- 记录锁(Record Lock):锁单条索引记录。
- 间隙锁(Gap Lock):锁索引区间,防止插入,解决幻读。
- 临键锁(Next-Key Lock):记录锁 + 间隙锁,锁前开后区间。
- 意向锁:表级,标识表内有行锁,加速判断是否有冲突。
Q4. Spring 事务的传播行为?
答:
| 传播行为 | 说明 |
|---|---|
| REQUIRED(默认) | 有事务加入,没有则新建 |
| REQUIRES_NEW | 总是新建,挂起当前事务 |
| NESTED | 嵌套事务,外层回滚则内层也回滚,内层回滚不影响外层 |
| SUPPORTS | 有事务加入,没有则非事务执行 |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 |
| NEVER | 非事务执行,有事务则抛异常 |
| MANDATORY | 必须在事务中,否则抛异常 |
- 常用:REQUIRED(默认)、REQUIRES_NEW(独立事务,如日志记录)、NESTED(部分回滚)。
Q5. Spring 事务失效的场景?
答:
- 方法非 public:Spring AOP 默认只代理 public 方法。
- 自调用:同类中 A 方法调用 B 方法,B 的
@Transactional失效(未走代理)。- 解决:从容器获取代理对象调用,或
AopContext.currentProxy()。
- 解决:从容器获取代理对象调用,或
- 异常被 try-catch 吞掉:
@Transactional默认只对RuntimeException回滚,需rollbackFor = Exception.class。 - 抛出非 RuntimeException:默认不回滚,需配置
rollbackFor。 - 数据库不支持事务:如 MyISAM 引擎。
- 传播行为配置错误:如
NOT_SUPPORTED、NEVER。 - Bean 未被 Spring 管理:new 出来的对象无代理。
- 多线程:事务绑定当前线程,新线程无事务上下文。
Q6. @Transactional 原理?
答:
- 基于 AOP 代理:Spring 为
@Transactional标注的 Bean 创建代理对象。 - 执行流程:
- 调用代理方法。
- 代理拦截,开启事务(
TransactionManager)。 - 执行目标方法。
- 正常返回 → 提交事务;抛异常 → 回滚(按
rollbackFor判断)。 - 释放资源。
- 核心接口:
PlatformTransactionManager:事务管理器。TransactionDefinition:事务定义(传播、隔离、超时、只读)。TransactionStatus:事务状态。
Q7. 分布式事务怎么处理?
答:
- 2PC(两阶段提交):协调者 prepare → commit。问题:单点、阻塞、数据不一致。
- 3PC:CanCommit → PreCommit → DoCommit。引入超时,缓解阻塞。
- TCC(Try-Confirm-Cancel):业务层面两阶段,需写补偿代码。高性能但侵入。
- Saga:长事务拆分为多个本地事务 + 补偿事务。适合长流程。
- 本地消息表:业务表 + 消息表同事务,定时扫描消息表发 MQ。最终一致。
- 事务消息(RocketMQ):半消息 → 本地事务 → commit/rollback。最终一致。
- Seata:
- AT 模式:自动生成回滚 SQL,无侵入。
- TCC 模式:业务补偿。
- Saga 模式:长事务编排。
- XA 模式:强一致性。
十二、排序算法与查找
Q1. 常见排序算法的时间/空间复杂度?
答:
| 算法 | 平均时间 | 最坏时间 | 空间 | 稳定 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(n²) | O(1) | ✓ |
| 选择 | O(n²) | O(n²) | O(1) | × |
| 直接插入 | O(n²) | O(n²) | O(1) | ✓ |
| 希尔 | O(n^1.3) | O(n²) | O(1) | × |
| 快排 | O(nlogn) | O(n²) | O(logn) | × |
| 归并 | O(nlogn) | O(nlogn) | O(n) | ✓ |
| 堆排序 | O(nlogn) | O(nlogn) | O(1) | × |
| 基数 | O(d·n) | O(d·n) | O(n) | ✓ |
Q2. 冒泡排序代码?
答:
java
public void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
boolean swapped = false; // 优化:无交换说明已有序
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
swapped = true;
}
}
if (!swapped) break;
}
}- 每轮把最大元素「冒泡」到末尾,每轮比较次数减 1。
Q3. 直接插入排序代码?
答:
java
public void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i]; // 待插入元素
int j = i - 1;
// 向左找第一个比 key 小的位置,沿途元素后移
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入
}
}- 类似扑克牌整理:把第 i 个元素插入前 i-1 个有序序列中。
- 小数据量优势:近乎有序时效率高,常作为快排/归并的子过程。
Q4. 二分查找代码?前提和注意事项?
答:
- 前提:数组有序、随机访问(链表不行)。
java
// 标准版(非递归)
public int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) { // 注意 <=
int mid = left + (right - left) / 2; // 防溢出
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}- 变体:
- 查找第一个等于 target:
arr[mid] >= target时right = mid - 1,最后检查left。 - 查找最后一个等于 target:
arr[mid] <= target时left = mid + 1。
- 查找第一个等于 target:
- 注意:
mid = (left + right) / 2在 left、right 大时溢出,应用left + (right - left) / 2。- 循环条件
<=还是<取决于区间定义。
Q5. 快速排序原理和代码?
答:
- 思想:分治。选一个 pivot,小于放左,大于放右,递归处理左右子数组。
- 平均 O(nlogn),最坏 O(n²)(已有序且选首尾为 pivot)。优化:三数取中、随机 pivot。
java
public void quickSort(int[] arr, int l, int r) {
if (l >= r) return;
int pivot = arr[l], i = l, j = r;
while (i < j) {
while (i < j && arr[j] >= pivot) j--;
while (i < j && arr[i] <= pivot) i++;
if (i < j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; }
}
arr[l] = arr[i]; arr[i] = pivot;
quickSort(arr, l, i - 1);
quickSort(arr, i + 1, r);
}Q6. 归并排序原理?
答:
- 分治:递归拆半 → 合并有序子数组。
- 稳定,O(nlogn),但需 O(n) 额外空间。
- 应用:外部排序(大数据分块排序后归并)、链表排序、求逆序对。
Q7. 堆排序原理?
答:
- 堆:完全二叉树,父节点 ≥/≤ 子节点(大/小顶堆)。
- 步骤:
- 建堆:从最后一个非叶子节点向上调整,O(n)。
- 排序:堆顶(最大)与末尾交换,堆大小减 1,下沉调整,重复。
- 特点:原地排序,O(nlogn),不稳定。
面试技巧
- 不会就说不会,别硬编。
- 答到点上:先说结论,再展开原理,最后结合项目场景。
- 主动引导:往自己熟悉的方向引(如被问 MySQL,主动提一下项目里的优化案例)。
- 代码题:先讲思路再写代码,边写边解释。
- 面完问面试官反馈:厚脸皮问表现和建议,下次改进。
高频追问预演
微服务
- Q:你们微服务怎么划分的?依据什么?
- Q:服务间调用超时怎么处理?重试会不会导致重复扣款?
- Q:网关怎么做限流?令牌桶和漏桶区别?
Redis
- Q:缓存雪崩、缓存击穿、缓存穿透怎么解决?
- Q:缓存和数据库一致性怎么保证?(延迟双删、Canal、消息)
- Q:Redis 为什么这么快?(内存、单线程避免上下文切换、IO 多路复用、高效数据结构)
HashMap
- Q:ConcurrentHashMap 在 1.7 和 1.8 的实现区别?
- Q:为什么树化阈值是 8?(泊松分布,概率极低)
JVM
- Q:线上 OOM 怎么排查?(jmap dump、MAT 分析)
- Q:CPU 100% 怎么排查?(top 找进程、jstack 找线程、找死循环或 GC 频繁)
- Q:怎么选择 GC 收集器?(堆大小、停顿要求、吞吐量)
xxl-job
- Q:xxl-job 调度中心高可用怎么做?(多节点 + DB 一致性锁)
- Q:任务执行失败怎么重试?(配置重试次数 + 失败告警)
MQ
- Q:怎么实现延迟消息?
- Q:消息积压 100 万条怎么办?
- Q:如何保证消费者处理顺序又不影响吞吐?
负载均衡
- Q:Nginx 的负载均衡策略有哪些?(轮询、加权、ip_hash、fair、url_hash)
- Q:服务网格(Istio)的负载均衡在哪一层?
Docker/K8s
- Q:Docker 镜像分层原理?怎么减小镜像大小?(多阶段构建、Alpine 基础镜像)
- Q:K8s 的 Service 怎么找到 Pod?(kube-proxy + iptables/ipvs + Endpoints)
- Q:Pod 一直在 Pending 怎么排查?(资源不足、调度约束、镜像拉取失败)
MySQL
- Q:联合索引 (a,b,c),哪些查询能走索引?
- Q:explain 中的 type、Extra、key_len 各代表什么?
- Q:慢 SQL 怎么优化?
设计模式
- Q:项目里用了什么设计模式?解决了什么问题?
- Q:JDK 里有哪些设计模式?(IO 流装饰者、集合迭代器、Runnable 适配器)
事务
- Q:Spring 事务和数据库事务什么关系?
- Q:什么时候用 REQUIRES_NEW?(日志、审计,独立提交不影响主流程)
- Q:分布式事务你们怎么做的?(Seata AT / 本地消息表 / RocketMQ 事务消息)
算法
- Q:为什么快排平均比归并快?(常数因子小、原地、缓存友好)
- Q:百万数据找 TopK?(小顶堆,O(nlogk))
背诵建议:
- 先背框架(每个问题的要点 1-2-3)。
- 再填充细节(数字、对比表)。
- 最后结合项目场景讲一遍,加深印象。
- 每天复习 2-3 个模块,循环 3-4 遍即可烂熟。