面试速查

Reference

Java 面试速查参考文档。覆盖 JVM、并发、集合、设计模式、Spring、Redis、消息队列、高可用、SQL、数据库选型与 HTTP 协议等,按左侧导航浏览。

持续更新

JVM 总览

JVM 是 Java 程序的运行环境,负责把字节码加载到内存中,通过解释执行或 JIT 编译运行,同时管理内存、线程、垃圾回收和运行时安全。

ClassLoader 运行时数据区 执行引擎 GC JNI

核心组成

类加载子系统

加载、验证、准备、解析、初始化 class 文件,把字节码变成 JVM 可以使用的运行时结构。

运行时数据区

包括堆、方法区、虚拟机栈、本地方法栈、程序计数器,是对象、线程栈帧和类元数据的主要存放区域。

执行引擎

负责执行字节码,包含解释器、JIT 编译器和垃圾回收器。

本地接口

通过 JNI 调用 C/C++ 等本地方法,常见于底层系统能力或高性能库。

内存模型

运行时内存结构

区域 线程共享 存放内容 常见异常
对象实例、数组,是 GC 管理的主要区域 OutOfMemoryError: Java heap space
方法区 / 元空间 类元数据、常量池、静态变量、JIT 编译后的代码等 OutOfMemoryError: Metaspace
虚拟机栈 方法调用的栈帧、局部变量表、操作数栈、返回地址 StackOverflowError
本地方法栈 Native 方法调用信息 StackOverflowErrorOutOfMemoryError
程序计数器 当前线程执行的字节码行号指示器 通常不会 OOM

堆内存分代

  • 新生代:大多数对象朝生夕死,通常分为 Eden、Survivor From、Survivor To。
  • 老年代:长期存活对象、大对象或经历多次 Minor GC 后晋升的对象。
  • 元空间:JDK 8 后替代永久代,使用本地内存,主要存储类元数据。

Java 内存模型 JMM

JMM 不是 JVM 运行时内存区域,而是 Java 并发语义规范,定义线程如何通过主内存和工作内存交互,解决可见性、有序性和原子性问题。

  • 原子性:基本读写通常具备原子性,复合操作如 i++ 不具备原子性。
  • 可见性:一个线程修改共享变量后,其他线程能否及时看到。volatile、锁、final 可提供可见性保障。
  • 有序性:编译器和 CPU 可能重排序,JMM 通过 happens-before 规则约束可观察结果。
面试注意:运行时数据区和 JMM 经常被混着问。回答时先区分:前者是 JVM 内存划分,后者是并发内存语义。

垃圾回收与调优

对象是否存活

  • 引用计数法:实现简单,但无法解决循环引用,JVM 主流 GC 不采用它作为核心判断方式。
  • 可达性分析:从 GC Roots 出发,能被引用链到达的对象是存活对象,否则可回收。

常见 GC Roots

  • 虚拟机栈中引用的对象,例如局部变量。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。
  • 被同步锁持有的对象。

垃圾回收算法

算法 特点 适用场景
标记-清除 先标记可回收对象再清除,会产生内存碎片 基础算法,老年代早期方案
复制算法 把存活对象复制到另一块区域,清理整块旧区域,效率高但浪费空间 新生代
标记-整理 标记后把存活对象向一端移动,解决碎片问题 老年代
分代收集 根据对象生命周期把堆分代,不同区域使用不同算法 现代 JVM 常见整体策略

常见垃圾回收器

  • Serial:单线程,简单稳定,适合客户端或小内存场景。
  • Parallel:吞吐量优先,适合批处理、计算任务。
  • CMS:低停顿老年代回收器,存在内存碎片问题,已逐步淘汰。
  • G1:面向服务端的低停顿收集器,把堆划分为多个 Region,可预测停顿时间。
  • ZGC / Shenandoah:面向超低停顿和大堆场景,停顿时间通常非常短。

调优思路

  1. 先明确目标:降低停顿、提升吞吐、减少 OOM,还是稳定响应时间。
  2. 收集证据:开启 GC 日志,结合监控、堆转储、线程栈分析。
  3. 判断问题类型:内存泄漏、对象创建过快、晋升失败、元空间不足、Full GC 频繁。
  4. 再调参数:堆大小、年轻代比例、GC 器选择、停顿目标、元空间上限。
  5. 压测验证:调优必须用真实流量模型或压测结果闭环。

常用参数

-Xms2g -Xmx2g
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc*:file=gc.log:time,uptime,level,tags
面试回答关键:GC 调优不是上来就改参数,而是先通过 GC 日志和监控确认问题,再结合业务目标选择收集器和参数,最后用压测验证。

类加载机制

类加载过程

  1. 加载:通过类的全限定名获取二进制字节流,转成方法区运行时数据结构,并在堆中生成 Class 对象。
  2. 验证:确保字节码符合 JVM 规范,不危害虚拟机安全。
  3. 准备:为类变量分配内存并设置默认初始值。
  4. 解析:把常量池中的符号引用转换为直接引用。
  5. 初始化:执行类构造器 <clinit>(),给静态变量赋代码中定义的值,执行静态代码块。
  6. 使用:程序正常访问类和对象。
  7. 卸载:类加载器可回收、类无实例、Class 对象无引用时,类才可能被卸载。

双亲委派模型

类加载器收到加载请求后,先委托父加载器尝试加载,父加载器无法加载时,子加载器才自己加载。

  • Bootstrap ClassLoader:加载 Java 核心类库。
  • Extension / Platform ClassLoader:加载扩展类库,JDK 9 后称 Platform ClassLoader。
  • Application ClassLoader:加载应用 classpath 下的类。
  • Custom ClassLoader:用户自定义类加载器,常见于热部署、插件化、隔离加载。

双亲委派的好处

  • 避免核心类被重复加载。
  • 保护 Java 核心 API,防止用户自定义 java.lang.String 等类替换核心类。
  • 保证类加载的一致性和安全性。

什么时候会打破双亲委派

  • JDBC、JNDI、SPI 等需要父加载器反向使用子加载器加载的实现类。
  • Tomcat 等容器需要隔离不同 Web 应用的类。
  • OSGi、插件化、热部署场景需要更灵活的类加载策略。

面试回答模板

问题:请你讲一下 JVM 内存结构

JVM 运行时数据区主要分为线程共享和线程私有两类。线程共享区域包括堆和方法区,堆主要存放对象实例,是 GC 的主要区域;方法区存放类元数据、常量、静态变量等。线程私有区域包括虚拟机栈、本地方法栈和程序计数器,虚拟机栈保存每次方法调用的栈帧,程序计数器记录当前线程执行位置。

问题:线上频繁 Full GC 怎么排查

我会先看 GC 日志和监控,确认 Full GC 的频率、耗时、触发原因以及各代内存变化。然后结合堆 dump 分析是否存在内存泄漏或大对象堆积;如果是对象创建过快,需要优化代码或缓存策略;如果是老年代空间不足或晋升失败,再考虑调整堆大小、年轻代比例或 GC 收集器参数。所有调整都要通过压测验证。

问题:双亲委派模型是什么,为什么需要它

双亲委派是类加载器加载类时先委托父加载器加载,父加载器加载不了再由自己加载。它的主要价值是保证核心类库的安全和唯一性,避免应用自己定义同名核心类破坏 JVM 行为,同时减少类的重复加载。

常见追问

  • JMM 和 JVM 内存结构有什么区别?JMM 是并发语义规范,关注可见性、有序性、原子性;JVM 内存结构是运行时数据区划分。
  • 为什么新生代适合复制算法?因为新生代对象大多很快死亡,存活对象少,复制成本低且能快速清理整块区域。
  • G1 为什么适合低停顿?G1 把堆拆成多个 Region,优先回收收益高的 Region,并可通过停顿目标控制回收节奏。
  • 类什么时候初始化?主动使用类时初始化,例如创建实例、访问静态变量、调用静态方法、反射调用、初始化子类前初始化父类。

多线程并发总览

并发编程要解决三类问题:如何创建和管理线程、如何保证共享数据安全、如何提升吞吐并控制资源消耗。生产环境通常用线程池而不是裸建线程,用锁或 CAS 保证一致性,用 JUC 工具类简化协作。

线程池 synchronized ReentrantLock volatile CAS AQS

线程状态

  • NEW → RUNNABLE → RUNNING / BLOCKED / WAITING / TIMED_WAITING → TERMINATED
  • sleep() 不释放锁;wait() 释放锁并进入等待队列。
  • yield() 让出 CPU,不保证立刻切换;join() 等待目标线程结束。

线程池

为什么用线程池

  • 降低频繁创建/销毁线程的开销。
  • 控制并发度,避免线程过多导致上下文切换和 OOM。
  • 统一管理任务提交、执行、拒绝和监控。

ThreadPoolExecutor 七大参数

参数含义
corePoolSize核心线程数,即使空闲也保留(除非 allowCoreThreadTimeOut)
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
unit存活时间单位
workQueue任务队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
threadFactory创建线程的工厂,可定制线程名便于排查
handler拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy

任务提交流程

  1. 当前线程数 < corePoolSize → 创建核心线程执行。
  2. 否则尝试入队;队列未满则排队。
  3. 队列满且线程数 < maximumPoolSize → 创建非核心线程。
  4. 否则执行拒绝策略。

常见线程池(Executors 工厂方法)

  • newFixedThreadPool:固定大小 + 无界队列,任务堆积时可能 OOM。
  • newCachedThreadPool:可扩容 + SynchronousQueue,高并发下线程数可能暴涨。
  • newSingleThreadExecutor:单线程顺序执行。
  • newScheduledThreadPool:定时/周期任务。
阿里规范建议:生产环境显式 new ThreadPoolExecutor,不用 Executors 快捷方法,以便明确队列边界和拒绝策略。

ForkJoinPool

分治任务专用,工作窃取(work-stealing)减少空闲;适合 CPU 密集型、可拆分的大任务,与 Stream 并行流底层相关。

锁机制

synchronized

  • 可修饰实例方法、静态方法、代码块;自动加锁解锁,不可中断、不可设置超时。
  • JDK 6 后锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(减少不必要的 OS 互斥)。
  • 保证原子性、可见性、有序性(通过 monitor 与内存屏障)。

ReentrantLock

  • 基于 AQS 实现,可重入;支持 lockInterruptibly()tryLock(timeout)
  • 可选公平锁(按申请顺序)或非公平锁(吞吐更高,默认)。
  • 需配合 Condition 实现多条件等待,类似多个 wait/notify 队列。

读写锁 ReadWriteLock

读多写少场景:读锁共享、写锁独占,提升读并发。StampedLock 支持乐观读,适合读远多于写。

volatile

  • 保证可见性、禁止部分重排序;不保证复合操作原子性(如 i++)。
  • 典型场景:状态标志位、双重检查锁中的实例引用发布。

锁对比

机制优点注意点
synchronized语法简单、JVM 优化成熟功能相对固定
ReentrantLock可中断、可超时、多 Condition必须 finally 中 unlock
volatile轻量、无阻塞不能替代锁做复合更新

死锁四要素与避免

  • 互斥、占有且等待、不可抢占、循环等待。
  • 避免:固定加锁顺序、tryLock 超时、缩小锁粒度、使用无锁结构。

CAS 与 AQS

CAS(Compare-And-Swap)

比较内存中的值与期望值,相等则更新为新值,是 CPU 原子指令实现的无锁乐观更新。

  • 优点:无阻塞、适合低竞争下的高性能计数、栈/队列头尾更新。
  • ABA 问题:值从 A→B→A,CAS 仍成功;可用 AtomicStampedReference 带版本号解决。
  • 自旋开销:竞争激烈时 CAS 反复失败,CPU 空转,此时锁可能更合适。

常用原子类

  • AtomicInteger / AtomicLong:基本类型原子操作。
  • AtomicReference:引用原子替换。
  • LongAdder:高并发计数,分段累加,比 AtomicLong 竞争小时更快。

AQS(AbstractQueuedSynchronizer)

JUC 锁与同步器的基础框架:state 表示同步状态,CLH 队列管理阻塞线程。

  • ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等均基于 AQS。
  • 独占模式 vs 共享模式;tryAcquire / tryRelease 由子类实现。

并发面试回答模板

问题:线程池参数怎么设

先看任务类型:CPU 密集型可接近 CPU 核数;IO 密集型可更大,如核数 × (1 + IO耗时/CPU耗时)。必须明确队列是否有界、拒绝策略和线程命名,并结合监控观察队列堆积和活跃线程数,避免无界队列导致 OOM。

问题:synchronized 和 ReentrantLock 区别

synchronized 是关键字,JVM 层面自动释放锁,有锁升级优化;ReentrantLock 是 API,基于 AQS,支持可中断、超时、公平锁和多 Condition。简单同步用 synchronized,需要高级控制用 ReentrantLock。

常见追问

  • ThreadLocal 内存泄漏?线程池复用线程时,若未 remove,Entry 的 key 弱引用被回收后 value 仍可能被强引用持有。
  • CAS 一定比锁快吗?不一定,高竞争下自旋浪费 CPU,应选锁或 LongAdder 等分段结构。

Lambda 表达式

函数式接口(仅一个抽象方法)的简洁实现,配合方法引用简化代码。

语法要点

  • (参数) -> 表达式(参数) -> { 语句; }
  • 类型可推断时可省略参数类型;单参数可省略括号。
  • 方法引用:类::静态方法实例::方法类::实例方法构造器::new
// 函数式接口
@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

Calculator add = (a, b) -> a + b;
List<String> names = Arrays.asList("Tom", "Jerry");
names.forEach(System.out::println);

内置函数式接口

接口方法用途
Predicate<T>test断言/过滤
Function<T,R>apply映射转换
Consumer<T>accept消费副作用
Supplier<T>get延迟供给

接口默认方法与静态方法

Java 8 允许接口有 defaultstatic 方法,便于 API 演进而不破坏实现类。

Stream API

对集合进行声明式、可链式、可并行的数据处理流水线,不存储元素,不修改源(除非明确可变操作)。

操作分类

  • 中间操作(惰性):filter、map、flatMap、distinct、sorted、peek、limit、skip
  • 终端操作(触发执行):forEach、collect、reduce、count、anyMatch、findFirst、min/max
List<User> result = users.stream()
    .filter(u -> u.getAge() >= 18)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .distinct()
    .collect(Collectors.toList());

// 分组与统计
Map<String, Long> byDept = users.stream()
    .collect(Collectors.groupingBy(User::getDept, Collectors.counting()));

并行流 parallelStream

  • 底层 ForkJoinPool.commonPool(),适合 CPU 密集型、无共享可变状态的任务。
  • 避免:IO 密集、小数据量、依赖顺序、涉及 synchronized 或非线程安全集合。
面试常问:Stream 与 Collection 区别 — Stream 是计算视图,单次消费;Collection 是数据结构,可多次遍历。

CompletableFuture

Java 8 增强的异步编程模型,支持组合、链式回调和异常处理,弥补 Future 只能阻塞 get 的不足。

常用 API

  • supplyAsync / runAsync:异步有返回值 / 无返回值
  • thenApply / thenAccept / thenRun:串行依赖上一步结果
  • thenCombine / allOf / anyOf:多任务组合
  • exceptionally / handle:异常恢复
  • orTimeout / completeOnTimeout(Java 9+):超时控制
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchOrder(), executor)
    .thenApply(order -> enrich(order))
    .exceptionally(ex -> { log.error("fail", ex); return "DEFAULT"; });

// 等待多个任务
CompletableFuture.allOf(f1, f2, f3).join();

与 Future / 回调对比

方式特点
Futureget 阻塞,难以链式组合
CompletableFuture非阻塞组合、显式线程池、异常传播可控
实践建议:始终传入自定义 Executor,避免默认 ForkJoinPool 与业务线程池混用;注意异常在链路上的传播,生产环境配合超时与熔断。

JDK 17 特性(LTS)

JDK 17 是长期支持版本,生产升级常选 11 → 17。以下为面试高频点。

语言与 API

  • 密封类 Sealed Classes:限制谁可以继承/实现,配合模式匹配更清晰建模。
  • 模式匹配 instanceof:判断类型同时绑定变量,减少强转样板代码。
  • Records:不可变数据载体,自动生成构造器、equals、hashCode、toString。
  • 文本块 Text Blocks:多行字符串 """ ... """,保留格式。
  • Switch 表达式增强-> 语法、yield、模式匹配 switch(预览演进中)。
public sealed interface Shape permits Circle, Rect {}
public record Point(int x, int y) {}

if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}

JVM 与库

  • 默认封装更强(强封装 JDK 内部 API),反射访问需 --add-opens
  • 移除 AOT/JIT 实验特性、Applet API、Security Manager 逐步废弃。
  • Foreign Function & Memory API(孵化/预览阶段演进)、Vector API 持续迭代。
  • macOS/AArch64 等平台支持成熟,ZGC、G1 等 GC 持续优化。
升级 17 时重点检查:反射/Agent、第三方框架版本、废弃 API(如 javax → jakarta 在 EE 场景)、构建插件与 CI 镜像。

JDK 21 特性(LTS)

JDK 21 是继 17 之后的 LTS,虚拟线程与若干预览特性进入主流面试题。

虚拟线程 Virtual Threads(Project Loom)

  • 轻量级线程,由 JVM 调度,适合高并发 IO 密集型(大量阻塞等待)。
  • Thread.startVirtualThread(runnable)Executors.newVirtualThreadPerTaskExecutor()
  • 不要池化虚拟线程;避免在虚拟线程中执行长时间 CPU 计算或 pin 住载体线程(如 synchronized 内阻塞,JDK 版本持续优化中)。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> fetchFromRemote(i)));
}

语言与其它重要特性

  • Record Patterns:解构 record,与 switch 模式匹配配合。
  • Pattern Matching for switch:正式版,类型模式 + null 处理更完整。
  • Sequenced Collections:统一首尾访问 API(getFirst、getLast、reversed)。
  • String Templates(预览):更安全、可扩展的字符串插值。
  • Generational ZGC:分代 ZGC 降低开销,改善吞吐与停顿。
  • Key Encapsulation Mechanism API:后量子密码相关标准 API。

17 → 21 选型简述

场景建议
稳定存量、框架未就绪优先 JDK 17 LTS
高并发 IO、微服务、新项目评估 JDK 21 + 虚拟线程
强依赖反射 hack 内部 API先解决模块化与 --add-opens 再升级

现代 Java 面试回答模板

问题:Stream 和 for 循环怎么选

简单遍历或性能敏感的热点路径可用 for;需要链式转换、分组、并行且逻辑声明式时用 Stream。Stream 有装箱和流水线开销,小集合不一定更快。

问题:CompletableFuture 如何避免线程池耗尽

为异步任务使用独立、有界线程池,区分 IO 与 CPU 任务;设置超时和拒绝策略;避免在 thenApply 里做阻塞 IO 占满默认池;大批量任务用分批或消息队列削峰。

问题:虚拟线程适合什么场景

适合大量短生命周期、以阻塞 IO 为主的任务,例如 HTTP 客户端调用、数据库访问。不适合 CPU 密集计算;与平台线程混用时注意线程本地变量和锁 pinning 问题。

常见追问

  • Lambda 捕获的变量为什么必须是 effectively final?避免并发语义下闭包内外状态不一致,简化实现。
  • Record 和 Lombok @Data 区别?Record 是语言级不可变语义,语义更清晰;Lombok 是编译期生成样板代码的普通类。
  • JDK 17 和 21 为什么都标 LTS?Oracle 每两年一个 LTS,17 和 21 都是长期支持节点,企业可按生态成熟度选择。

集合框架体系总览

java.util 集合框架以接口为核心,常用实现类在底层用数组、链表、哈希表或红黑树组织数据。选型时看:是否有序、是否允许重复/null、读写比例、是否并发。

Collection List Set Map Queue

继承关系(面试常画)

  • IterableCollectionList / Set / Queue
  • Map 独立接口,不继承 Collection;entrySet() 可当作 Set 视图操作

选型速查

需求推荐底层
随机访问、尾部追加多ArrayList动态数组
头尾插入删除多LinkedList双向链表
去重、无序、查快HashSetHashMap 的 key
去重且保持插入顺序LinkedHashSet哈希表 + 双向链表
去重且排序TreeSet红黑树
K-V、查改快HashMap数组 + 链表/红黑树
高并发读多写少ConcurrentHashMap分段/CAS + 桶锁
高并发读多、写极少CopyOnWriteArrayList写时复制数组

List 底层实现

ArrayList

  • 底层 Object[] elementData,默认初始容量 10(首次 add 时才真正分配)。
  • 扩容:容量不足时 grow(),新容量约为 old + (old >> 1),即约 1.5 倍,再 Arrays.copyOf 拷贝。
  • 随机访问 get(i) O(1);尾部 add 均摊 O(1);中间插入/删除需数组搬移 O(n)。
  • 非线程安全;fail-fast:迭代中结构修改抛 ConcurrentModificationException(modCount 检测)。

LinkedList

  • 双向链表,节点含 itemprevnext;同时实现 Deque,可作栈/队列。
  • 头尾插入删除 O(1);按索引访问需遍历 O(n)。
  • 内存开销大于 ArrayList(每个元素额外两个引用)。

Vector / Stack

  • Vector 方法 synchronized,线程安全但粒度粗、性能差,已被淘汰思路。
  • 需要线程安全 List 用 Collections.synchronizedList 或并发集合,而非 Vector。
面试结论:绝大多数场景用 ArrayList;只有频繁在头/中间插入删除且数据量大时才考虑 LinkedList。

Set 底层实现

Set 不允许重复元素,HashSetLinkedHashSet 内部本质是 HashMap(或 LinkedHashMap),元素存在 key 上,value 是固定占位对象 PRESENT

HashSet

  • 依赖 hashCode() 定位桶,equals() 判断相等;只重写 equals 不重写 hashCode 会破坏契约。
  • 无序、允许一个 null(JDK 8+ HashMap 树化后仍支持 null key 的单 null 限制在 HashMap 层)。

LinkedHashSet

  • 在 HashSet 基础上维护双向链表记录插入顺序(或访问顺序,构造参数 accessOrder)。
  • 略增内存,适合需要稳定遍历顺序的去重场景。

TreeSet

  • 底层 TreeMap,红黑树保证有序,增删查 O(log n)。
  • 元素需实现 Comparable 或构造时传入 Comparator;不允许 null(比较时会 NPE)。

Map 底层实现

HashMap(JDK 8+)

  • 结构:数组(桶)+ 链表;链表长度 ≥ 8 且数组长度 ≥ 64 时链表转红黑树;节点 ≤ 6 时树退化为链表。
  • 默认负载因子 0.75:平衡空间与时间,过大则链表/树变长,过小则频繁扩容。
  • 容量为 2 的幂:索引 (n - 1) & hash,等价取模且更快。
  • hash 扰动:高 16 位与低 16 位异或,减少低位相同导致的冲突。
  • put 流程:算索引 → 桶空则放头节点 → 否则比 key(先比 hash 再 equals)→ 冲突则尾插/树插 → 超阈值则 resize。
  • resize:容量翻倍,节点要么在原索引,要么在「原索引 + 旧容量」(利用 e.hash & oldCap 分流,JDK 8 优化)。
  • 非线程安全;多线程 put 可能死循环(JDK 7 头插扩容)或数据丢失,JDK 8 改为尾插但仍不安全。
// 扰动与寻址(概念)
hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
index = (table.length - 1) & hash;

LinkedHashMap

  • 继承 HashMap,额外维护双向链表:after 指针串联顺序。
  • accessOrder=false:插入顺序;true:访问顺序,可用于 LRU 缓存(配合 removeEldestEntry)。

TreeMap

  • 红黑树按 key 排序;ComparatorComparable 决定顺序。
  • 有序、O(log n),不适合 hash 均匀分布的高吞吐纯查找(HashMap 均摊 O(1) 更优)。

Hashtable

  • 方法级 synchronized,不允许 null key/value;遗留类,用 ConcurrentHashMap 替代。

HashMap vs Hashtable vs ConcurrentHashMap

HashMapHashtableConcurrentHashMap
线程安全是(粗粒度锁)
null允许 1 个 null key,多 null value不允许不允许
JDK 8 结构数组+链表+红黑树数组+链表数组+链表+红黑树
锁粒度整表桶头 synchronized / CAS

并发集合

ConcurrentHashMap(JDK 8+)

  • 取消 Segment 分段锁(JDK 7),改为对每个桶头节点 synchronized + CAS 插入空桶。
  • size 用 baseCount + CounterCell[] 分散计数,减少竞争。
  • 扩容支持多线程协助转移(ForwardingNode),复杂度高于 HashMap 单线程 resize。
  • get 一般无锁(volatile 读可见性);迭代弱一致性,不抛 CME。

CopyOnWriteArrayList / CopyOnWriteArraySet

  • 写时复制:修改时复制新数组再替换引用,读无锁、写成本高。
  • 适合读多写极少(白名单、监听器列表);写频繁或数据量大时内存与复制开销大。

其它

  • ConcurrentLinkedQueue:CAS 无界非阻塞队列。
  • BlockingQueue 家族:ArrayBlockingQueue(有界数组+一把锁)、LinkedBlockingQueue(可选有界链表)、PriorityBlockingQueue、DelayQueue、SynchronousQueue 等,用于线程池与生产者消费者。

集合面试回答模板

问题:HashMap 底层原理,为什么用红黑树

JDK 8 的 HashMap 是数组加链表,冲突严重时链表转红黑树,把最坏查找从 O(n) 降到 O(log n)。负载因子 0.75、容量 2 的幂是为了平衡扩容频率和哈希分布。put 时先算扰动后的 hash 定位桶,再比较 key,冲突则链表或树节点插入,超过阈值就扩容并 rehash。

问题:HashMap 线程不安全体现在哪

多线程同时 put 可能覆盖彼此的结果,扩容时也可能出现丢失或(JDK 7 头插)死循环。并发场景用 ConcurrentHashMap,不要用 Collections.synchronizedMap 包一层应付高并发(整表锁粒度大)。

问题:ArrayList 和 LinkedList 区别

ArrayList 基于动态数组,随机访问快、尾部添加均摊快,中间插入删除要搬移元素。LinkedList 是双向链表,头尾插入删除快,按索引访问要遍历。实际开发中 ArrayList 更常用,LinkedList 除非明确需要 Deque 语义或频繁头尾操作,否则优势不明显。

常见追问

  • hashCode 和 equals 契约?相等对象 hash 必须相同;hash 相同对象可不等,故还要 equals。
  • 为什么容量是 2 的幂?位运算取模、扩容时 rehash 可优化(JDK 8 高低位分流)。
  • Comparable 和 Comparator?前者内置于类,自然排序;后者外部策略,可多种排序规则。
  • fail-fast 和 fail-safe?前者迭代中检测 modCount 快速失败;后者(如 COW、CHM 迭代器)弱一致,不保证看到最新且一般不抛 CME。
  • 如何用 LinkedHashMap 做 LRU?构造 accessOrder=true,重写 removeEldestEntry 在超过容量时删除最久未访问条目。

SOLID 设计原则

面向对象设计的五条基本原则,目标是降低耦合、提高可维护性与可扩展性。

原则英文含义违反时的症状
S Single Responsibility 一个类只应有一个引起变化的原因(一个职责) 类臃肿、改一处牵动多处、难测试
O Open/Closed 对扩展开放,对修改关闭 新需求只能改老代码、if-else 膨胀
L Liskov Substitution 子类必须能替换父类且不改变程序正确性 子类重写后破坏父类契约、需 instanceof 特判
I Interface Segregation 客户端不应依赖它不需要的接口 「胖接口」、空实现、被迫实现无用方法
D Dependency Inversion 高层模块不依赖低层实现,都依赖抽象 业务层直接 new 具体类、难以替换与单测

面试记忆与示例

  • S:订单服务只处理订单逻辑,邮件通知拆到 NotificationService
  • O:促销规则用策略接口扩展,而不是在结算里不断加 if (双11)
  • L:正方形不应继承长方形(改一边会破坏另一边语义);子类不能加强前置、削弱后置条件。
  • I:Worker 拆成 WorkableEatable,机器人不必实现 eat()
  • D:Service 依赖 UserRepository 接口,由 Spring 注入 UserRepositoryImpl
SOLID 不是教条:微服务边界、框架约束、性能场景下可权衡;面试要说清「为什么」和「代价」。

创建型模式

关注对象的创建过程,将「如何 new」与业务逻辑解耦。

单例 Singleton

  • 保证全局唯一实例:配置中心、连接池管理器、Spring 单例 Bean(容器级)。
  • 写法:饿汉、懒汉、静态内部类、枚举(推荐,防反射与序列化破坏)。
  • 注意:多 ClassLoader、集群环境下「单例」语义不同;过度使用难测试。

工厂方法 Factory Method

  • 定义创建对象的接口,由子类决定实例化哪个具体类。
  • 示例:LoggerFactory.getLogger() 按配置返回不同 Logger 实现。

抽象工厂 Abstract Factory

  • 创建一族相关/依赖的产品(UI 主题:Button + TextField 成套)。
  • 与工厂方法:抽象工厂是「多产品族」,工厂方法是「单产品多实现」。

建造者 Builder

  • 分步构建复杂对象,链式调用,参数可选多。
  • 示例:Lombok @BuilderStringBuilder、HTTP Request 构建、MyBatis SqlSessionFactoryBuilder

原型 Prototype

  • 通过克隆已有对象创建新对象,避免重复昂贵初始化。
  • Java:Cloneable 浅拷贝;深拷贝需手动或序列化。Spring prototype scope 类似思想。

结构型模式

关注类与对象的组合,形成更大结构并保持灵活。

适配器 Adapter

  • 将不兼容接口转为客户端期望的接口。
  • 类适配器(继承)vs 对象适配器(组合,更常用)。
  • 示例:InputStreamReader 把字节流适配为字符流;对接第三方 SDK。

装饰器 Decorator

  • 动态给对象添加职责,比继承更灵活,可层层包装。
  • 示例:BufferedInputStream 包装 FileInputStreamCollections.synchronizedList
  • 与代理:装饰器侧重「增强功能」,代理侧重「控制访问」。

代理 Proxy

  • 为对象提供代理以控制访问、延迟加载、权限、日志、事务。
  • 静态代理:手写代理类;JDK 动态代理:基于接口;CGLIB:子类继承,可代理类。
  • Spring AOP、MyBatis Mapper、RPC 客户端均为典型应用。

外观 Facade

  • 为子系统提供统一简化入口,降低调用复杂度。
  • 示例:SLF4J 门面、封装多个微服务调用的聚合 Service。

其它(了解)

  • 桥接 Bridge:抽象与实现分离,如消息发送(邮件/短信)× 平台(Windows/Mac)。
  • 组合 Composite:树形结构统一对待叶子与容器,如菜单/文件夹。
  • 享元 Flyweight:共享细粒度对象,如字符串常量池、棋类游戏棋子状态共享。

行为型模式

关注对象之间的通信、职责分配与算法封装。

策略 Strategy

  • 封装一族算法,使它们可互换;消除条件分支。
  • 示例:Comparator、支付渠道(微信/支付宝)、路由规则、促销计算。

模板方法 Template Method

  • 父类定义算法骨架,子类重写某些步骤(钩子方法)。
  • 示例:AbstractList、Servlet service()、JUnit 测试生命周期、Spring JdbcTemplate

观察者 Observer

  • 一对多依赖:主题状态变化时通知所有观察者。
  • Java:Observable(已过时);Spring 事件 ApplicationEvent、MQ 发布订阅、Listener。

责任链 Chain of Responsibility

  • 请求沿处理者链传递,直到有人处理。
  • 示例:Servlet Filter 链、Netty Pipeline、审批流、日志级别链。

状态 State

  • 对象内部状态改变时改变其行为,用类代替大段 switch。
  • 示例:订单状态机(待支付→已支付→已发货)、TCP 连接状态。

命令 Command

  • 将请求封装为对象,支持撤销、排队、日志。
  • 示例:Runnable、事务命令、GUI 宏命令。

常用模式速查表

模式一句话JDK / 框架中的影子
单例全局唯一Runtime、Spring 单例 Bean
工厂封装创建Calendar.getInstance、LoggerFactory
建造者分步构建StringBuilder、Lombok Builder
适配器接口转换InputStreamReader
装饰器动态增强IO 包装流
代理控制访问Spring AOP、MyBatis Mapper
外观统一入口SLF4J
策略算法可换Comparator
模板方法固定流程JdbcTemplate
观察者发布订阅Spring Event、MQ
责任链链式处理Filter、Interceptor

设计面试回答模板

问题:说说你理解的 SOLID

单一职责让一个类只承担一类变化;开闭原则通过抽象和多态扩展而不是改旧代码;里氏替换保证子类可安全替换父类;接口隔离避免胖接口;依赖倒置让业务依赖接口,由 DI 注入实现。实际项目中我会结合 Spring 和策略/模板等模式落地,而不是为了原则而过度设计。

问题:代理模式在 Spring 里怎么体现

Spring AOP 默认对带接口的 Bean 用 JDK 动态代理,无接口时用 CGLIB 生成子类代理。切面逻辑(事务、日志、权限)织入目标方法前后,调用方注入的是代理对象,从而在不改业务代码的前提下增强功能,符合开闭原则。

问题:策略模式和 if-else 怎么选

分支少且稳定用 if-else 更直观;规则会频繁增加、需要独立测试或运行时切换时用策略模式,把每种算法封装成实现类,由上下文或工厂选择,符合开闭原则。支付渠道、计费规则是典型场景。

常见追问

  • 单例模式线程安全写法?推荐静态内部类或枚举;双重检查锁需 volatile 防指令重排。
  • 装饰器和代理区别?意图不同:装饰器扩展功能、可叠加;代理控制访问、常代表真实对象。
  • 工厂方法和抽象工厂区别?前者创建一个产品的一种实现;后者创建多个相关产品的一组实现。
  • 设计模式是否越多越好?否,过度设计增加复杂度;在变化点使用合适模式即可。

Spring Boot / Cloud 体系总览

Spring Boot 简化单体/微服务应用的启动与配置(自动配置、Starter、内嵌容器)。Spring Cloud 在 Boot 之上提供分布式能力:注册发现、配置中心、负载均衡、熔断限流、网关、链路追踪等。

Starter AutoConfiguration Nacos OpenFeign Gateway Sentinel

常见组件对照(面试版)

能力常见实现
注册与发现Nacos、Eureka、Consul
配置中心Nacos Config、Spring Cloud Config
负载均衡Spring Cloud LoadBalancer(替代 Ribbon)
服务调用OpenFeign、RestTemplate + LB
熔断限流Sentinel、Resilience4j(Hystrix 已停更)
API 网关Spring Cloud Gateway、Kong
链路追踪Micrometer Tracing、SkyWalking、Zipkin

自动配置原理

启动流程(精简)

  1. @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
  2. 运行 SpringApplication.run() → 创建 ApplicationContext(Servlet 环境多为 AnnotationConfigServletWebServerApplicationContext)
  3. 加载 application.yml/properties、环境 Profile、Bean 定义
  4. 执行各类 *AutoConfiguration,按条件注册 Bean
  5. 内嵌 Tomcat/Jetty/Undertow 启动 Web 容器

@EnableAutoConfiguration 做了什么

  • 通过 @Import(AutoConfigurationImportSelector.class) 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 2.7+ / 3.x;旧版为 spring.factories
  • 候选配置类经 @ConditionalOnXxx 过滤后生效

常用条件注解

注解含义
@ConditionalOnClassclasspath 存在某类
@ConditionalOnMissingBean容器中尚无该 Bean 时才注册默认实现
@ConditionalOnProperty配置项开关,如 xxx.enabled=true
@ConditionalOnWebApplicationWeb 应用环境

Starter 机制

  • Starter = 依赖聚合 + 自动配置模块,如 spring-boot-starter-webspring-boot-starter-data-redis
  • 自定义 Starter:autoconfigure 模块 + AutoConfiguration.imports + 配置属性类 @ConfigurationProperties
@Configuration
@ConditionalOnClass(RedisTemplate.class)
@EnableConfigurationProperties(RedisProperties.class)
public class MyRedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(...) { ... }
}
排查「Bean 未加载」:看是否被条件注解排除、是否被自定义 Bean 覆盖(OnMissingBean)、Profile 与包扫描路径是否正确。

循环依赖

什么是循环依赖

Bean A 依赖 B,B 又依赖 A(或更长环 A→B→C→A)。Spring 通过三级缓存解决单例 + 字段/Setter 注入场景下的循环依赖。

三级缓存(Singleton)

缓存名称内容
一级singletonObjects完全初始化好的单例 Bean
二级earlySingletonObjects早期暴露的 Bean(已实例化,可能未填完属性)
三级singletonFactoriesObjectFactory,用于生成早期引用(AOP 代理对象在此介入)

解决流程(A 依赖 B,B 依赖 A)

  1. 创建 A:实例化后放入三级缓存,继续属性注入发现需要 B
  2. 创建 B:实例化后放入三级缓存,注入 A 时从三级拿到 A 的早期引用(可能是代理)
  3. B 完成初始化,进入一级缓存
  4. A 拿到 B,完成初始化,进入一级缓存

无法解决的情况

  • 构造器注入循环依赖:实例化前就要 B,无法提前暴露半成品 → 启动失败
  • prototype 作用域:每次 new,不参与单例三级缓存
  • 多例 + 单例混合环、非 Spring 管理的 new 对象

实践建议

  • 优先重构:拆服务、事件驱动、@Lazy 延迟注入(治标)
  • 避免构造器环;必要时改用 Setter/字段注入并明确文档
  • Spring Boot 2.6+ 默认禁止部分循环依赖行为可配置:spring.main.allow-circular-references=true(不推荐长期依赖)

服务治理

微服务治理关注:服务如何被发现、如何可靠调用、如何限流熔断、如何统一配置与可观测。

注册与发现

  • 服务启动时向注册中心(Nacos/Eureka)注册 IP、端口、元数据、健康状态
  • 消费者从注册中心拉取/订阅服务列表,结合负载均衡选择实例
  • Nacos 同时支持 CP(Raft 持久实例)与 AP(临时实例)模式;Eureka 偏 AP(自我保护)

配置中心

  • 配置与代码分离,支持动态刷新(@RefreshScope + Spring Cloud Context)
  • 多环境 Namespace/Group、灰度发布、加密配置(Jasypt)

负载均衡与调用

  • @LoadBalanced RestTemplateWebClient + LoadBalancer 拦截
  • OpenFeign:声明式 HTTP 客户端,集成 LB、熔断、请求压缩;底层常为 HttpClient / OkHttp
  • 负载策略:轮询、随机、权重(Nacos 权重)、同 zone 优先等

熔断、限流、降级

  • Sentinel:QPS/并发线程数限流、熔断(慢调用比例、异常比例、异常数)、热点参数、系统自适应保护
  • 熔断状态:关闭 → 打开(快速失败)→ 半开(探测恢复)
  • 降级:返回默认值、缓存、友好提示,保证核心链路可用

其它治理能力

  • 分布式事务:Seata(AT/TCC/Saga)、最终一致性(消息表、可靠消息)
  • 链路追踪:TraceId 透传,分析慢调用与依赖拓扑
  • 服务契约:API 文档(OpenAPI)、版本兼容、消费者驱动契约(可选)

Spring Cloud Gateway

基于 Spring WebFlux + Netty 的响应式 API 网关,统一入口:路由、过滤、限流、鉴权、跨域、灰度。

核心概念

  • Route:路由 ID + 目标 URI + Predicate 集合 + Filter 集合
  • Predicate:断言匹配请求(Path、Method、Header、Cookie、时间窗口等)
  • Filter:Gateway Filter(路由级)与 Global Filter(全局),可改请求/响应

请求链路(简化)

  1. 客户端 → Gateway
  2. Global Filter 链(如负载均衡、限流、鉴权)
  3. 匹配 Route → 执行 Gateway Filter
  4. 通过 LoadBalancer 选择下游服务实例 → 转发
  5. 响应回传,可统一加 Header、日志、熔断
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

Gateway vs Zuul

GatewayZuul 1.x
模型WebFlux 非阻塞Servlet 阻塞
性能高并发下更易扩展线程池瓶颈明显
维护Spring 官方主推已进入维护态

典型能力实现

  • 统一鉴权:Global Filter 校验 JWT,下游信任网关或二次校验
  • 灰度发布:根据 Header/用户标签路由到金丝雀实例(结合 Nacos 元数据)
  • 限流:RequestRateLimiter + Redis;或与 Sentinel Gateway 适配器集成
  • 跨域:Cors WebFilter 或 Gateway 全局 CORS 配置

Spring 面试回答模板

问题:Spring Boot 自动配置原理

启动类上的 @EnableAutoConfiguration 会导入 AutoConfigurationImportSelector,读取 META-INF 下自动配置类列表,再结合 @ConditionalOnClass、OnMissingBean 等条件决定是否注册 Bean。Starter 把依赖和这些自动配置打包在一起,实现开箱即用;需要定制时用户自己声明同类型 Bean 即可覆盖默认实现。

问题:Spring 如何解决循环依赖

对单例 Bean 使用三级缓存:实例化后通过 ObjectFactory 提前暴露早期引用,让另一个 Bean 能先注入这个半成品,完成初始化后再放入一级缓存。若涉及 AOP,早期引用可能是代理对象。构造器注入的循环依赖无法这样解决,需要改设计或使用 @Lazy。

问题:Gateway 在架构中的作用

Gateway 作为南北向统一入口,负责路由转发到注册中心里的微服务实例,并在网关层做鉴权、限流、日志、跨域和灰度。它基于 WebFlux 非阻塞模型,适合高并发 API 场景,与 Nacos、LoadBalancer、Sentinel 等组件配合完成服务治理。

常见追问

  • @Bean 和 @Component 区别?@Component 是类路径扫描的通用组件;@Bean 是在 @Configuration 里用方法显式注册,适合第三方类。
  • Feign 和 RestTemplate?Feign 声明式、可集成熔断与契约;RestTemplate 更底层,需手写 URL 与 LB。
  • Nacos 和 Eureka?Nacos 兼具注册与配置;Eureka 仅注册发现,2.x 停更后新项目多选 Nacos/Consul。
  • 为什么 Gateway 用 WebFlux?少量线程处理大量 IO 等待,适合网关转发;注意阻塞调用需调度到 boundedElastic 避免阻塞事件循环。

Redis 总览

Redis 是基于内存的 KV 数据库,单线程执行命令(6.0+ 多线程主要处理网络 IO),支持丰富数据结构、持久化、主从复制、哨兵与 Cluster。常用于缓存、会话、排行榜、分布式锁、消息队列等。

单线程模型 RDB/AOF 主从 Sentinel Cluster

为什么快

  • 内存读写 + 高效数据结构
  • 单线程避免锁竞争(命令仍原子)
  • IO 多路复用(epoll)处理大量连接
  • 纯内存操作,持久化异步进行

数据结构

五大基础类型

类型底层(常见)典型场景常用命令
String SDS(简单动态字符串) 缓存对象、计数器、分布式锁、Session GET/SET、INCR、SETNX、EXPIRE
List quicklist(链表 + ziplist 压缩) 消息队列、最新列表(注意双端操作复杂度) LPUSH/RPOP、BLPOP、LRANGE
Hash ziplist 或 hashtable 对象字段存储(用户信息),小 Hash 省内存 HGET/HSET、HMGET、HINCRBY
Set intset 或 hashtable 标签、共同好友、去重、抽奖 SADD、SINTER、SUNION、SRANDMEMBER
ZSet(Sorted Set) skiplist + hashtable 排行榜、延迟队列、范围查询 ZADD、ZRANGE、ZRANK、ZINCRBY

高级 / 扩展类型

  • Bitmap:位操作,签到、活跃用户统计(BITCOUNT 需注意大数据耗时)
  • HyperLogLog:基数估算(UV),省内存,有误差
  • GEO:地理位置,底层 ZSet,附近的人
  • Stream:消息流,消费者组,类似轻量 MQ(持久化、ACK、Pending)
  • Pub/Sub:发布订阅,不持久化,订阅方离线会丢消息

键与过期

  • 过期策略:定期删除 + 惰性删除(访问时检查是否过期)
  • 内存淘汰(maxmemory-policy):noeviction、allkeys-lru、volatile-lru、allkeys-lfu 等
  • 生产建议:缓存必须设 TTL;热点 key 注意过期时间打散

持久化

方式特点
RDB快照,恢复快,可能丢最后一次快照后的数据
AOF记录写命令,数据更完整,文件大需 rewrite
混合AOF 重写时嵌入 RDB 前缀,兼顾恢复速度与完整性

集群与高可用

三种部署形态

模式作用说明
主从复制 读写分离、数据备份 主写从读;异步复制有延迟;从节点可做冷备
哨兵 Sentinel 自动故障转移 监控主从,主宕机选举新主;客户端连哨兵获取当前主地址
Cluster 水平扩展、分片 16384 个 hash slot,多主多从;无中心架构,Gossip 通信

Cluster 核心机制

  • key → CRC16(key) % 16384 → slot → 负责该 slot 的 master 节点
  • MOVED:slot 不在当前节点,客户端应更新槽位映射并重试
  • ASK:迁移中的临时重定向
  • Hash Tag:{userId}:profile{userId}:order 同 slot,保证事务/Multi 可用
  • 最少 3 主 3 从(官方建议),主宕机从节点晋升

复制与一致性

  • 主从异步复制:主成功写后返回客户端,再同步从库 → 主宕机可能丢少量已写未同步数据
  • wait 命令:可等待 N 个从库确认(慎用,影响延迟)
  • 脑裂:网络分区导致多主,需合理配置 min-slaves-to-write 等(版本与方案各异)
面试选型:数据量小、高可用 → 主从 + Sentinel;数据量大、需分片 → Cluster。云托管 Redis 通常封装了高可用与扩缩容。

缓存问题与解决方案

三大经典问题

问题现象方案
缓存穿透 查询不存在的数据,缓存和 DB 都没有,请求打到 DB 布隆过滤器;缓存空值(短 TTL);接口校验与限流
缓存击穿 热点 key 过期瞬间,大量请求同时查 DB 互斥锁(Redis SETNX)只放一个线程回源;逻辑过期(值带过期时间,异步刷新);热点 key 永不过期 + 后台更新
缓存雪崩 大量 key 同时过期或 Redis 宕机,DB 压力激增 TTL 加随机抖动;多级缓存;集群高可用;限流降级;Redis 持久化与备份

缓存与数据库一致性

  • Cache Aside(常用):读先缓存未命中再 DB 并回写;写先更新 DB 再删缓存(推荐删而非立刻更新,避免并发写乱序)
  • 延迟双删:删缓存 → 写 DB → 延迟再删一次,降低脏读窗口
  • 强一致场景:分布式锁、 Canal/MQ 订阅 binlog 异步删缓存、读主库
  • 接受最终一致性时,短 TTL + 主动失效是性价比最高的方案

热点 Key(Hot Key)

  • 本地缓存(Caffeine)挡一层;key 拆分(分片到多个 Redis key)
  • 读写分离、多副本;大促前预热;监控 QPS 异常 key

大 Key(Big Key)

  • 危害:阻塞单线程、迁移慢、内存不均、网卡打满
  • 治理:redis-cli --bigkeys、拆分 Hash/List、压缩、异步 UNLINK 删除

分布式锁(常考)

  • 基础:SET key uuid NX EX 30,解锁用 Lua 校验 uuid 再 DEL,防误删他人锁
  • 问题:主从切换可能导致锁丢失 → Redisson 看门狗续期、Redlock(有争议,生产需评估)
  • 强一致锁可考虑 ZooKeeper / etcd;Redis 锁适合允许极低概率重复的业务
// 互斥回源(击穿)伪代码
String v = redis.get(key);
if (v == null) {
    if (redis.setnx(lockKey, "1", "EX", 10)) {
        try {
            v = db.load(key);
            redis.setex(key, ttl, v);
        } finally {
            redis.del(lockKey);
        }
    } else {
        Thread.sleep(50);
        return get(key); // 重试或短暂降级
    }
}

Redis 面试回答模板

问题:Redis 为什么单线程还快

数据在内存,命令执行无磁盘随机 IO;单线程避免锁和上下文切换;使用 epoll 等多路复用处理大量连接。6.0 以后多线程主要分担网络读写,命令执行仍大体保持单线程语义。瓶颈通常在内存大小、网络带宽和大 key 慢操作,而不是 CPU 多核。

问题:缓存穿透、击穿、雪崩区别与解决

穿透是数据根本不存在,用布隆过滤器或空值缓存挡掉无效请求。击穿是单个热点 key 过期导致并发打 DB,用互斥锁或逻辑过期只让一个请求回源。雪崩是大量 key 同时失效或 Redis 不可用,用 TTL 随机化、集群高可用、限流降级和多级缓存来削峰。

问题:Redis Cluster 如何分片

集群把 16384 个 slot 分布在多个 master 上,客户端对 key 做 CRC16 取模得到 slot,再路由到对应节点。扩容时迁移 slot,期间用 ASK 重定向。需要多 key 事务时用 hash tag 保证相关 key 落在同一 slot。

常见追问

  • String 底层 SDS 好处?O(1) 取长度、二进制安全、减少扩容频率。
  • ZSet 为什么用跳表?实现简单,范围查询 O(log n),并发下比红黑树更易实现。
  • 先删缓存还是先写库?先写库再删缓存更常见;先删缓存再写库并发下易出现脏缓存。
  • Redis 和 Memcached?Redis 多数据结构、持久化、主从集群;Memcached 简单纯缓存、多线程。

消息队列总览

消息队列解耦、异步、削峰填谷。选型常看:吞吐、延迟、顺序、事务消息、运维生态、与现有技术栈契合度。

Topic Partition Consumer Group ACK 幂等

核心概念对照

概念RocketMQKafka
逻辑主题TopicTopic
分片单元Message Queue(Queue)Partition
消费组Consumer GroupConsumer Group
消费进度Broker 端 Offset(可集群消费)Broker 端 __consumer_offsets
顺序消息同一 Queue 有序同一 Partition 有序

RocketMQ vs Kafka 对比

维度RocketMQKafka
定位 业务消息中间件,阿里系电商/金融场景成熟 分布式流平台,日志采集、大数据管道、流计算首选
吞吐 高(万级~十万级 TPS 常见),略低于 Kafka 极限 极高(百万级 TPS 集群),顺序写磁盘 + Page Cache
延迟 毫秒级,支持定时/延迟消息级别丰富 毫秒级;延迟消息需自研或 Kafka 时间轮(版本特性)
消息模型 Topic + Queue;支持 Tag 过滤、SQL92 过滤 Topic + Partition;依赖 Consumer 或 Streams 处理
事务消息 原生半消息 + 回查(本地事务与消息一致性) 事务 API(0.11+),主要用于流式 exactly-once 生态
顺序消息 全局/分区顺序,Sharding Key 绑定 Queue 分区内有序,key 决定 partition
堆积能力 亿级堆积,依赖磁盘与消费速度 日志型存储,超长堆积与回溯是强项
消费模式 集群消费、广播消费 Consumer Group 负载均衡;独立 Consumer 可重复读
运维与生态 NameServer 轻量;国内文档与 Spring 集成多 ZK/KRaft、Connect、Streams、Flink/Spark 生态强
典型场景 订单、支付、库存、分布式事务、定时任务 日志、埋点、CDC、实时数仓、流处理

架构差异(简图理解)

  • RocketMQ:Producer → Broker(Master/Slave)← NameServer 路由;Consumer 拉取或长轮询 Push 效果
  • Kafka:Producer → Broker(Leader/Follower ISR)← Controller/KRaft;Consumer 拉取(poll),消费位点提交
选型口诀:业务交易、事务消息、延迟消息偏 RocketMQ;海量日志、流计算、超高吞吐偏 Kafka。很多公司两者并存。

消息可靠性

可靠性 = 消息不丢、不重复(或重复可接受)、可追踪。需从生产 → 存储 → 消费全链路设计。

生产者不丢

MQ手段
RocketMQ 同步发送 + 重试;事务消息;发送失败落库补偿任务
Kafka acks=all(ISR 全部确认);retriesenable.idempotence=true 防重
  • acks=0:不等待确认,可能丢
  • acks=1:Leader 写入即返回,Leader 宕机未同步可能丢
  • acks=all/-1:ISR 副本都确认,最可靠(需 min.insync.replicas 配合)

Broker 不丢

  • RocketMQ:同步刷盘 / 异步刷盘;同步复制 / 异步复制;Master-Slave 故障切换
  • Kafka:Partition 多副本 + ISR;replication.factor;unclean.leader.election 关闭可降低丢数据风险
  • 磁盘 RAID、监控磁盘使用率与副本滞后(Lag)

消费者不丢

  • 先执行业务再提交 offset(或 RocketMQ CONSUME_SUCCESS 后再确认)
  • 消费失败:重试队列 + 死信队列(DLQ)人工介入
  • 自动提交 offset 可能在处理中途崩溃导致「以为已消费」→ 手动提交更可控

RocketMQ 事务消息流程

  1. 发送 Half 消息(对消费者不可见)
  2. 执行本地事务
  3. Commit → 消息可见;Rollback → 丢弃
  4. Broker 回查本地事务状态(超时或未决时)

至少一次 / 至多一次 / 精确一次

语义含义实现要点
At most once可能丢,不重复先提交 offset 再消费
At least once不丢,可能重复先消费再提交 offset + 消费端幂等
Exactly once不丢不重Kafka 事务 + Streams;或业务层幂等表

幂等设计

MQ 普遍提供 至少一次(At Least Once) 投递,网络重试、Rebalance、消费超时会导致重复消费,业务必须幂等。

幂等层次

  • 生产幂等:Kafka enable.idempotence(PID + Sequence);业务侧用业务 ID 去重表
  • 消费幂等:核心,靠唯一键、状态机、数据库唯一约束

常见消费幂等方案

方案做法适用
唯一键 + DB 订单号/消息 ID 唯一索引,重复插入失败即已处理 创建类操作
幂等表 记录 messageId,处理前 INSERT,成功则标记;重复则跳过 通用
Redis SETNX messageId 作为 key,过期时间略大于重试窗口 高并发、允许极低 Redis 故障风险
状态机 仅允许 待支付→已支付,重复「已支付」消息直接返回成功 订单、工单流转
版本号/乐观锁 UPDATE ... WHERE version = ?,影响行数 0 则已处理或冲突 更新类操作
业务 Token 先申请一次性 Token,消费时校验并销毁 表单重复提交场景

设计要点

  • 消息体携带全局唯一 messageIdbizId,与业务主键对齐
  • 幂等记录与业务写放在同一本地事务,避免「记了幂等但业务失败」
  • 消费逻辑本身要可重入:外部调用(支付、发短信)也需幂等或先查状态
  • 重试次数与退避策略,最终进 DLQ,防止毒消息无限循环
// 幂等表伪代码
@Transactional
void onMessage(Message msg) {
    if (idempotentRepo.exists(msg.getMessageId())) {
        return; // 已处理
    }
    idempotentRepo.insert(msg.getMessageId()); // 唯一索引防并发
    orderService.createOrder(msg.getBody());
}
面试强调:MQ 保证不丢靠 ACK 与持久化;不重复靠「MQ 有限能力 + 业务幂等」组合,不要指望只调参数就 exactly-once。

MQ 面试回答模板

问题:RocketMQ 和 Kafka 怎么选

Kafka 擅长超高吞吐和流式生态,适合日志、埋点、实时数仓;RocketMQ 在业务消息、顺序、延迟、事务消息方面更贴近电商金融场景,和国内 Spring 集成成熟。若核心是订单支付一致性我会倾向 RocketMQ 事务消息;若是百万级日志管道选 Kafka。

问题:如何保证消息不丢

生产端同步发送并配置 acks=all 或 RocketMQ 同步刷盘与同步复制;Broker 多副本并监控 ISR/Lag;消费端先完成业务再提交 offset,失败走重试和死信。全链路还要有加锁、补偿任务和监控告警。

问题:如何处理重复消费

承认 MQ 通常是至少一次语义,在消费端做幂等:用 messageId 或业务唯一键配合数据库唯一索引、幂等表或 Redis 去重,结合状态机只推进合法状态。生产端 Kafka 可开幂等生产者减少重复写入。

常见追问

  • 消息堆积怎么办?扩容 Consumer、增加 Partition/Queue、优化消费逻辑、临时降级非核心消费、排查慢 SQL。
  • 顺序消息如何实现?同一 Sharding Key 进同一 Queue/Partition;消费单线程或分区锁。
  • 死信队列作用?重试耗尽后隔离毒消息,人工修复或丢弃,保护主队列。
  • Rebalance 影响?消费暂停、重复消费风险上升,应控制单次拉取量、优雅下线。

CAP / BASE 理论

CAP(分布式系统三选二)

  • C(Consistency)一致性:所有节点同一时刻读到相同数据
  • A(Availability)可用性:每个请求都能得到非错误响应(不保证最新)
  • P(Partition tolerance)分区容错:网络分区时系统仍能继续工作

分布式环境下网络分区不可避免,因此实际在 CPAP 之间权衡:

类型选择典型
CP牺牲可用性,保证强一致ZooKeeper、etcd、HBase、Redis 集群故障时部分不可用
AP牺牲强一致,保证可用Eureka、Cassandra、DNS

BASE(互联网大规模实践)

  • BA(Basically Available)基本可用:降级、限流、故障转移,核心功能可用
  • S(Soft state)软状态:允许中间状态存在一段时间
  • E(Eventually consistent)最终一致性:经过一段时间后达到一致
面试:CAP 是理论模型;生产用 BASE + 业务补偿实现「可接受的弱一致」,而不是追求处处强一致。

秒杀系统设计

核心矛盾:读多写少、瞬时超高并发、库存不能超卖。思路:流量层层递减,库存扣减原子化,异步削峰。

分层架构

  1. 前端:按钮防重复、验证码、活动未开始静态化、CDN 静态资源
  2. 接入层:Nginx/Gateway 限流、黑名单、IP 频控、页面缓存
  3. 应用层:活动页数据 Redis 预热;登录态校验;秒杀接口独立集群
  4. 服务层:Redis 预减库存(Lua 原子 DECR);成功则发 MQ 异步创建订单
  5. 数据层:DB 最终扣减 + 唯一约束防超卖;订单服务幂等

防超卖手段

  • Redis Lua:if stock > 0 then decr end 原子判断扣减
  • 数据库:UPDATE stock SET n=n-1 WHERE id=? AND n>0 影响行数为 0 则失败
  • 一人一单:用户 ID + 活动 ID 唯一索引或 Redis Set
  • 禁止:先查库存再 update 的非原子「读-改-写」

常见问题

问题对策
缓存击穿活动数据预热、逻辑过期
DB 被打穿Redis 挡量 + MQ 异步写库
恶意刷单风控、验证码、设备指纹、限流
库存预热不一致以 DB 为准定时校准;活动开始锁定库存

分布式事务

跨服务、跨库、跨 MQ 的一致性。强一致成本高,互联网多采用最终一致性

方案对比

方案一致性特点
2PC / XA 强一致 协调者两阶段提交;阻塞、性能差,微服务少用
TCC 最终/准强 Try-Confirm-Cancel,业务侵入大,需空回滚、防悬挂
本地消息表 最终一致 业务与消息同库事务,定时扫描发 MQ
事务消息(RocketMQ) 最终一致 半消息 + 本地事务 + 回查
Saga 最终一致 长事务拆本地事务 + 补偿;编排式或 choreography
Seata AT 弱强一致 自动生成回滚 SQL,对业务透明,依赖 undo_log

选型建议

  • 短链路、强一致要求:Seata AT / TCC(量不大时)
  • 订单+库存+支付:Saga 补偿或可靠消息 + 幂等
  • 原则:能避免分布式事务就避免(合并服务、单库);必须用时优先异步最终一致

分布式锁

在分布式环境下互斥执行:库存扣减、定时任务单实例、防重复提交。

实现对比

实现优点缺点
Redis(SET NX EX) 性能高、实现简单 主从切换可能丢锁;需 Lua 安全解锁;Redisson 看门狗续期
ZooKeeper / etcd 强一致、临时顺序节点公平 性能低于 Redis;运维成本高
数据库唯一索引 无额外组件 性能差、锁表风险
Redisson 可重入、看门狗、读写锁、联锁 依赖 Redis 集群可用性

正确使用要点

  • 锁 value 用 UUID,解锁 Lua 脚本校验「只删自己的锁」
  • 过期时间 > 业务最大耗时,或看门狗自动续期
  • 锁粒度尽量小;能不用锁则用 DB 乐观锁 / 原子 SQL
  • Redlock 有争议,金融强一致场景慎选 Redis 锁

限流

保护系统不被突发流量压垮,保证核心链路可用。可在网关、应用、Redis 多层实施。

常见算法

算法特点场景
固定窗口实现简单,边界处可能双倍突发粗粒度限流
滑动窗口更平滑,统计更准接口 QPS
漏桶(Leaky Bucket)恒定流出速率,削峰下游处理能力固定
令牌桶(Token Bucket)允许一定突发,平均速率受限Guava RateLimiter、Sentinel

实现层级

  • 网关:Spring Cloud Gateway RequestRateLimiter、Nginx limit_req
  • 框架:Sentinel 按资源名 QPS/线程数、热点参数限流
  • 分布式:Redis + Lua 全局限流;按用户/IP/接口维度

与降级区别

  • 限流:超过阈值直接拒绝或排队,保护系统
  • 降级:关闭非核心功能,返回兜底数据,保证主流程

熔断与降级

防止故障依赖拖垮整个系统(雪崩)。熔断快速失败;降级提供备选方案。

熔断器状态机(Hystrix / Sentinel / Resilience4j)

  1. Closed:正常调用,统计错误率/慢调用比例
  2. Open:超过阈值则熔断,直接失败或走 fallback,经过休眠时间后进入半开
  3. Half-Open:放行少量探测请求,成功则关闭熔断,失败则继续打开

触发条件(常见)

  • 错误比例 > 50%(滑动窗口内)
  • 慢调用比例 > 阈值(RT 超过设定 ms)
  • 异常数、并发线程数超限(Sentinel)

实践组合

手段作用
超时避免线程阻塞堆积
重试幂等接口有限次重试 + 退避
熔断依赖故障时快速失败
降级返回缓存/默认值,关闭推荐位
隔离线程池/信号量隔离,Bulkhead 舱壁模式
限流入口削峰
Sentinel 侧重限流+熔断规则可视化;Hystrix 已停更,新项目多用 Sentinel 或 Resilience4j。Feign 可集成 fallbackFactory 做降级。

高可用面试回答模板

问题:如何设计一个秒杀系统

我会把流量从前到后层层过滤:CDN 和静态页、网关限流、活动数据 Redis 预热。扣库存用 Redis Lua 原子预减,成功后再发 MQ 异步下单,数据库用条件更新和唯一索引防超卖,消费端幂等。全程配合监控、降级非核心接口,保证核心下单链路可用。

问题:CAP 怎么理解,和 BASE 关系

分区容错在分布式里必须考虑,所以实际是在一致性和可用性之间权衡。CP 如 ZooKeeper 选举期间可能不可用;AP 如 Eureka 保证注册服务可读但可能读到旧列表。BASE 是 AP 的延伸,通过基本可用和最终一致性满足互联网业务,配合消息、补偿和幂等落地。

问题:分布式事务怎么做

优先避免:合并服务或单库。必须跨服务时,强一致场景可用 Seata AT 或 TCC;大部分业务用可靠消息、事务消息或 Saga 补偿实现最终一致,关键是幂等和可对账。2PC 性能差,微服务里较少用。

问题:限流和熔断区别

限流是控制进入系统的请求速率,防止过载;熔断是下游故障时切断调用链,快速失败防止线程拖死。通常入口限流 + 调用下游超时重试 + 熔断降级组合使用,配合舱壁隔离线程池。

常见追问

  • 雪崩效应?依赖故障 → 线程阻塞 → 资源耗尽 → 级联失败;用超时、熔断、隔离、限流打破链条。
  • TCC 空回滚?Try 未执行却收到 Cancel,需识别并直接成功;悬挂是 Cancel 先于 Try,需防重。
  • 令牌桶 vs 漏桶?令牌桶允许突发;漏桶输出恒定,更适合平滑写下游。

SQL 调优总览

调优目标:更快返回(降低 RT)或更少资源(CPU/IO/扫描行数/Shuffle 量)。先定位瓶颈,再改 SQL、索引或架构。

通用排查步骤

  1. 看慢查询日志 / APM / 执行计划(EXPLAIN / EXPLAIN ANALYZE
  2. 确认是否全表扫描、错误索引、回表过多、Join 顺序差
  3. 检查数据量、统计信息是否过期(ANALYZE TABLE
  4. 改写 SQL、加/改索引、拆查询、预聚合、缓存
  5. 压测验证,对比 rows examined、执行时间、成本

MySQL EXPLAIN 关键列

关注点
typesystem > const > eq_ref > ref > range > index > ALL(尽量避免 ALL)
key实际使用的索引
rows预估扫描行数,越小越好
ExtraUsing filesort、Using temporary 需警惕

索引基本原则

  • 最左前缀:联合索引 (a,b,c) 可用于 a、ab、abc 条件
  • 区分度高的列放前面;避免对索引列函数/隐式类型转换(WHERE YEAR(dt)=2024 坏,范围改区间好)
  • 覆盖索引减少回表;写多读少表慎加索引

谓词下推(Predicate Pushdown)

WHEREHAVING 等过滤条件尽可能下推到数据源附近先执行,减少向上层传递的数据量,降低网络、内存与 CPU 成本。

关系型数据库(MySQL / PostgreSQL)

  • 优化器自动下推:能走索引的条件在存储引擎层过滤
  • 子查询改写:将过滤写在子查询内部,避免先全量再过滤
  • 视图/派生表:外层 WHERE 可能合并进内层(MySQL 5.7+ 条件下推优化)
-- 不推荐:外层才过滤
SELECT * FROM (
  SELECT * FROM orders WHERE status IN (1,2,3)
) t WHERE t.created_at >= '2024-01-01';

-- 推荐:条件下推至最内层
SELECT id, amount, created_at
FROM orders
WHERE status IN (1,2,3)
  AND created_at >= '2024-01-01'
  AND user_id = ?;

数仓 / 大数据(Hive、Spark SQL、Presto)

  • 分区裁剪:WHERE 带分区列(dt='2024-05-01')只读对应目录
  • 列裁剪:只 SELECT 需要的列,减少 ORC/Parquet 读取
  • 谓词下推至存储:文件级跳过 Stripe/Row Group(谓词 + 列存统计信息 min/max)
  • 避免在最外层才过滤几亿行明细;ETL 尽早 Filter

无法在 SQL 层下推时注意

  • 对计算列过滤:WHERE UPPER(name)='ABC' → 考虑生成列 + 索引或冗余列
  • 跨库 Join:先在各库内过滤再联邦 Join,减少中间结果
  • ORM 生成 SQL 丢失条件:检查 N+1、全表 load 再内存 filter
面试表述:谓词下推 =「尽早过滤、少传数据」;OLTP 靠索引+优化器,OLAP 靠分区+列存+下推至文件块。

Join 优化

Join 类型与代价

类型说明注意
INNER JOIN只保留匹配行最常用的优化对象
LEFT JOIN保留左表全部右表条件放 ON 还是 WHERE 结果不同
STRAIGHT_JOIN强制 MySQL 连接顺序仅在确认优化器选错时用

MySQL Join 算法(NLJ / BNL / Hash)

  • Nested Loop Join:驱动表逐行嵌套被驱动表;被驱动表需有索引(eq_ref/ref)
  • Block Nested Loop:无索引时缓存驱动表块,仍较慢
  • Hash Join(8.0.18+):等值连接大表无合适索引时,内存建哈希表

优化要点

  1. 小表驱动大表(传统 NLJ):减少外层循环次数;8.0 优化器常自动选驱动表
  2. 被驱动表 Join 列建索引:避免对被驱动表全表扫描
  3. 先过滤再 Join:各表 WHERE 后再 Join,缩小中间结果(谓词下推)
  4. 避免 Join 列类型/字符集不一致:导致无法走索引
  5. 控制 Join 表数量:超 3~4 张考虑宽表、物化中间表或业务拆分
  6. 数仓大表 Join:Map Join(广播小表)、Bucket Join(分桶对齐)、Skew Join 处理数据倾斜
-- LEFT JOIN 条件位置
-- 过滤右表且不要保留右表无匹配:条件放 WHERE
SELECT a.* FROM users a
LEFT JOIN orders b ON a.id = b.user_id
WHERE b.created_at >= '2024-01-01';  -- 无订单用户会被滤掉,语义等同 INNER

-- 保留左表全部:右表过滤放 ON
SELECT a.* FROM users a
LEFT JOIN orders b ON a.id = b.user_id AND b.created_at >= '2024-01-01';

子查询 vs Join

  • IN (SELECT ...) 有时可改 EXISTS 或 Join,便于走索引
  • 避免相关子查询逐行执行;改 Join 或窗口函数(MySQL 8.0)

报表查询提效与降本

报表多为读多写少、聚合重、时间范围大,目标:少扫明细、少重复计算、可缓存。

架构层手段

手段作用
读写分离报表走从库,隔离 OLTP
汇总表 / 宽表按天/店铺预聚合,查询只扫汇总行
物化视图定时刷新中间结果(MySQL 可定时任务写汇总表)
OLAP 引擎ClickHouse、Doris、StarRocks 做多维分析
缓存Redis 缓存热点报表;结果带版本与 TTL
异步导出超大报表走 MQ + 离线生成,避免拖垮在线库

SQL 层降本

  • 分区表:按日/月分区,查询必带分区键,避免扫描全部分区
  • 只查必要列:禁止 SELECT * 扫宽表
  • 分页优化:深分页 LIMIT 100000,20 慢 → 用上次最大 ID 游标:WHERE id > ? ORDER BY id LIMIT 20
  • 聚合下推:在数仓层 GROUP BY,应用层不再拉明细聚合
  • 近似计算:UV 用 HyperLogLog、采样统计降低精确度换速度
  • 冷热分离:历史数据归档到对象存储 + 外表查询

降低计算成本(云/集群)

  • 减少全表 COUNT(*):用统计表或估算(SHOW TABLE STATUS
  • 合并重复报表任务:同一汇总表多报表复用
  • 调度错峰:T+1 凌晨批处理,避免与高峰 OLTP 抢资源
  • Spark/Hive:控制 Shuffle 分区数、广播小维表、过滤前置、缓存复用中间 Dataset
  • 监控:扫描字节数、Spill 磁盘、任务时长,设资源配额

反模式(报表常见)

  • 在线库直接跑大范围 JOIN + GROUP BY + 排序
  • 实时库做复杂多维下钻无索引维度
  • 每次请求全量重算,无增量更新

SQL 面试回答模板

问题:慢 SQL 怎么排查和优化

先用慢日志和 EXPLAIN 看是否全表扫描、索引未命中、filesort 或临时表。然后检查 WHERE 是否命中联合索引最左前缀、是否有隐式转换。改写为先过滤再 Join、避免 SELECT *,必要时加覆盖索引或汇总表。数仓场景还要做分区裁剪和谓词下推,减少扫描数据量。

问题:什么是谓词下推

把过滤条件尽量推到离数据源最近的一层执行,在读文件或走索引时就丢掉不符合条件的行,减少后续 Join 和聚合的数据量。在 MySQL 里体现为优化器下推和子查询内写清条件;在 Hive/Spark 里还配合分区裁剪和列式存储的 Row Group 跳过。

问题:Join 很慢怎么优化

保证 Join 列有索引且类型一致;各表先 WHERE 缩小结果集;控制 Join 表数量。MySQL 让小结果集做驱动表,被驱动表走 ref/eq_ref。大表等值 Join 可看 8.0 Hash Join。数仓里对小维表用 Map Join,解决倾斜用随机前缀或单独处理热点 key。

问题:报表查询如何不影响线上又降成本

报表走从库或 OLAP,用按日汇总表和定时任务预计算,查询只扫聚合结果。SQL 上带分区键、避免深分页、异步导出大结果。批处理错峰,监控扫描量和 Shuffle,能近似就近似,历史数据归档。

常见追问

  • 覆盖索引?索引包含查询所需全部列,避免回表。
  • 索引失效场景?对列运算、不等于、前导模糊 %abc、OR 未全覆盖、类型转换。
  • filesort 一定慢?内存 sort buffer 够用可接受;数据量大考虑索引有序或降序需求改设计。
  • COUNT(1) vs COUNT(*)?InnoDB 下优化器处理相近,性能差异可忽略,看语义即可。

数据库选型总览

先分清 workload:OLTP(高并发短事务、强一致)vs OLAP(大批量扫描、聚合分析)vs HTAP(两者都要,有取舍)。

数据库定位一句话
MySQLOLTP互联网业务库首选,生态成熟
PostgreSQLOLTP + 复杂 SQL功能最全的开源关系库,扩展强
ClickHouseOLAP列存分析引擎,聚合极速
Hologres实时数仓 / HTAP云原生实时分析,兼容 PG 协议,离线与实时一体

OLTP vs OLAP

维度OLTPOLAP
典型操作INSERT/UPDATE 单行、点查大范围扫描、GROUP BY、JOIN 大表
数据模型行存、范式设计列存、宽表/星型模型
一致性强一致 ACID常最终一致,批量导入
扩展主从、分库分表分布式分片、MPP

MySQL / PostgreSQL / ClickHouse / Hologres 对比

维度 MySQL PostgreSQL ClickHouse Hologres
类型 关系型 OLTP 关系型 OLTP(可轻量 OLAP) 列存 OLAP 实时数仓(行列混合,HTAP 倾向)
存储引擎 InnoDB 行存为主 Heap 行存,扩展列存(cstore 等) MergeTree 系列列存 行列共存,Segment 存储,支持实时写入
事务 ACID 完整支持(InnoDB) 完整支持,MVCC 弱/无传统事务,批量导入为主 支持主键表事务语义;分析表偏批量
并发写入 高,行级锁 高,MVCC 读写不阻塞 批量写入优,高频单行更新弱 高吞吐实时写入(Flink 导入等)
点查 / 主键 极强,B+ 树索引 强,多种索引(B-tree、GIN、GiST) 主键查询可用但非强项 主键表支持高 QPS 点查 + 分析
复杂分析 弱,大表聚合慢 中等,窗口函数、CTE 强 极强,向量化执行 强,MPP 并行,面向 PB 级
JOIN 大表 吃力 中等 看引擎与内存,宽表更好 优化器强,适合数仓星型模型
更新删除 频繁 UPDATE/DELETE 同左 不推荐频繁改;Collapsing/Replacing 引擎 支持 UPSERT、增量更新,仍偏分析场景
SQL 兼容 MySQL 方言 标准 SQL 程度高 类 SQL,函数/语法有差异 PostgreSQL 协议兼容
扩展性 主从、中间件分片(ShardingSphere) 流复制、Citus 分布式 分片集群,本地表 + Distributed 表 云原生弹性扩缩容,存算分离
生态 Java 最熟、云 RDS 普及 GIS、JSON、时序(Timescale)插件 日志/监控/BI 集成广 阿里云、Flink/MaxCompute/DataWorks 一体
部署 自建/RDS 成熟 自建/RDS/云托管 自建/云 CK 服务 主要为阿里云托管
成本模型 按实例,OLTP 性价比好 同左,复杂查询需调优 存储压缩高,分析 QPS 成本低 按 CU/存储,实时数仓综合计费

各库核心特点(面试记忆)

MySQL

InnoDB B+ 树、redo/undo、主从复制;适合订单、用户、交易;分库分表成熟。

PostgreSQL

MVCC 无回滚段读阻塞、丰富索引与类型;适合复杂查询、地理、JSON、金融合规场景。

ClickHouse

列存 + 向量化 + 数据压缩;MergeTree 后台合并;适合日志分析、大屏、用户行为聚合。

Hologres

实时写入 + 交互式查询;与 Flink 双流、MaxCompute 离线打通;Serving 层点查 + 分析一体。

场景与典型架构

选型建议

场景推荐说明
电商订单、支付、账户MySQL / PG强事务、行级锁、成熟 ORM
复杂报表、存储过程、GISPostgreSQLSQL 能力强、扩展多
APP 埋点、日志、监控指标ClickHouse高压缩、聚合快、成本低
实时大屏、即席分析、湖仓一体(阿里云)HologresFlink 实时入仓 + 多维分析 + 在线 Serving
既要交易又要实时分析MySQL + CK/HologresCDC(Canal/Debezium)同步到分析库,勿硬扛 OLTP

Lambda / 湖仓数据流(常见)

  1. 业务库 MySQL → Binlog → Flink/Canal
  2. 实时层写入 Hologres 或 Kafka → ClickHouse
  3. 离线层 MaxCompute/Hive T+1 汇总,再回灌 Hologres 做融合查询
  4. BI / 报表 / API 查询走 OLAP,保护 OLTP

ClickHouse vs Hologres(常对比)

维度ClickHouseHologres
优势极致单表聚合、开源可控、压缩比高实时写入低延迟、PG 生态、阿里云数据栈集成、主键点查
劣势UPDATE 弱、分布式 Join 需建模厂商绑定、成本依赖云定价
更适合日志/指标、自助分析、成本敏感海量扫描实时数仓、Flink 实时链路、需要 PG 兼容的 Serving

MySQL vs PostgreSQL(常对比)

  • MySQL:简单易用、主从普及、互联网案例多;复杂 SQL、CTE 历史较弱(8.0 已改善)
  • PG:标准 SQL、窗口函数、CTE、扩展生态;同样规格下复杂查询往往更稳
  • 选型:团队熟悉度 + 云厂商支持 + 是否需要 PG 特有扩展
原则:没有万能库。OLTP 与 OLAP 分离是常态;用同步链路连接,而不是用 MySQL 硬跑亿级 GROUP BY。

数据库面试回答模板

问题:MySQL 和 ClickHouse 怎么分工

MySQL 做业务 OLTP,保证事务和点查更新。ClickHouse 接 CDC 或日志流做 OLAP,承担大范围聚合和报表。原因是存储模型不同:行存适合短事务,列存适合扫描聚合。混用会拖垮在线库,也发挥不出 CK 的压缩和向量化优势。

问题:Hologres 和 ClickHouse 区别

两者都面向分析,但 Hologres 强调实时写入与 PG 协议兼容,和 Flink、MaxCompute 组成阿里云实时数仓,并支持主键高 QPS 点查,偏 HTAP Serving。ClickHouse 开源生态广,单表聚合和成本极致,但高频更新弱,更适合日志指标类场景。选型看是否已在阿里云实时链路、是否需要 PG 兼容接口。

问题:为什么报表不用 MySQL 直接查

报表多为大范围扫描和聚合,InnoDB 行存会读大量无用列,锁和 buffer pool 压力大,影响 OLTP。应通过汇总表、从库或同步到 ClickHouse/Hologres 等 OLAP 引擎,配合分区、预聚合和列存压缩,既快又降本。

常见追问

  • ClickHouse 为何快?列存、压缩、向量化执行、少随机 IO、MPP。
  • PG 的 MVCC?每行多版本,读不阻塞写,需 VACUUM 回收死元组。
  • HTAP 是什么?一套系统同时扛 OLTP+OLAP,如 TiDB、Hologres 部分场景;一般要接受架构或性能权衡。
  • CDC 是什么?捕获数据库变更日志同步到数仓,保证分析侧近实时。

HTTP 协议总览

HTTP(HyperText Transfer Protocol)是应用层协议,基于 TCP(HTTP/3 基于 QUIC/UDP),采用请求-响应模型,默认无状态(状态靠 Cookie/Session/Token 维护)。

报文结构

请求:
请求行   GET /api/user HTTP/1.1
首部行   Host: example.com
         Content-Type: application/json
空行
消息体   {"id":1}

响应:
状态行   HTTP/1.1 200 OK
首部行   Content-Type: application/json
         Content-Length: 15
空行
消息体   {"name":"Tom"}

一次完整交互(简化)

  1. DNS 解析域名 → IP
  2. TCP 三次握手建立连接(HTTPS 再 TLS 握手)
  3. 客户端发送 HTTP 请求
  4. 服务端处理并返回响应
  5. 连接关闭(HTTP/1.0)或复用(Keep-Alive / HTTP/2 多路复用)

核心特性

  • 无状态:服务器不保存客户端上下文,利于水平扩展
  • 灵活可扩展:首部可扩展;REST 用 URI + 方法表达资源
  • 明文/加密:HTTP 明文;HTTPS = HTTP + TLS

方法与状态码

常用方法(REST 语义)

方法语义幂等安全典型用途
GET获取资源查询,不应有副作用
POST提交/创建创建资源、表单提交
PUT全量更新/替换整体替换资源
PATCH部分更新否*局部字段更新
DELETE删除删除资源
HEAD同 GET 无 body检查资源是否存在、元数据
OPTIONS查询支持的方法CORS 预检

* PATCH 幂等性视实现而定,面试可说「通常不保证」。

常见状态码

类别含义
1xx100Continue,继续发送 body
2xx 成功200OK
201Created,POST 创建成功
204No Content,成功无 body(DELETE 常见)
3xx 重定向301永久重定向,SEO 权重转移
302/307临时重定向(307 保持方法)
304Not Modified,协商缓存命中
4xx 客户端400Bad Request,参数错误
401Unauthorized,未认证
403Forbidden,无权限
404Not Found
409Conflict,资源冲突
429Too Many Requests,限流
5xx 服务端500Internal Server Error
502Bad Gateway,网关上游异常
503Service Unavailable,过载/维护
504Gateway Timeout,上游超时
401 vs 403:401「你是谁」→ 要登录;403「你是谁我知道,但不能访问」。

HTTP 版本演进

版本关键特性问题/改进
HTTP/1.0 每次请求新建 TCP 连接 连接开销大
HTTP/1.1 默认 Keep-Alive 持久连接;管道化(pipelining,少用了);分块传输 Chunked 队头阻塞(同一连接上请求串行应答)
HTTP/2 二进制分帧、多路复用、头部压缩 HPACK、服务端推送(Push,现较少用) TCP 层仍可能队头阻塞;单连接多 Stream
HTTP/3 基于 QUIC(UDP),独立 Stream,连接迁移 解决 TCP 队头阻塞,弱网更稳

队头阻塞(HOL Blocking)

  • HTTP/1.1:同一连接多个请求排队等待响应
  • HTTP/2:应用层多路复用,但 TCP 丢包会阻塞整个连接所有 Stream
  • HTTP/3:QUIC 在传输层按 Stream 独立重传,减轻 HOL

连接管理

  • Connection: keep-alive(1.1 默认持久)
  • 浏览器对同一域名并发连接数有限(HTTP/1.1 常 6 个),故雪碧图、域名分片;HTTP/2 单连接多路复用缓解

首部与缓存

常见请求首部

  • Host:虚拟主机必选(1.1)
  • User-AgentAcceptAccept-Encoding: gzip
  • Authorization: Bearer <token> / Basic 认证
  • Cookie:携带会话
  • Content-Type / Content-Length
  • If-None-MatchIf-Modified-Since:协商缓存验证

常见响应首部

  • Set-Cookie:HttpOnly、Secure、SameSite 防 XSS/CSRF
  • Cache-Control:缓存策略核心
  • ETag / Last-Modified:协商缓存标识
  • Location:重定向 URL

缓存机制

类型机制首部
强缓存 未过期直接用本地,不请求服务器 Cache-Control: max-age=3600;过期看 Expires(优先级低)
协商缓存 过期后问服务器是否变化,未变则 304 ETag + If-None-Match;或 Last-Modified + If-Modified-Since

优先级:Cache-Control > ExpiresETag 比时间戳更准。

Content-Type 常考

  • application/json:REST API
  • application/x-www-form-urlencoded:表单默认
  • multipart/form-data:文件上传
  • text/htmlapplication/octet-stream

CORS(跨域)

  • 浏览器同源策略:协议+域名+端口相同
  • 简单请求:直接发,响应带 Access-Control-Allow-Origin
  • 预检请求:OPTIONS 先发,带 Access-Control-Request-Method

HTTPS 与 TLS

HTTPS = HTTP + TLS(SSL 已淘汰),提供机密性、完整性、身份认证

TLS 握手(1.2 简化)

  1. Client Hello:支持的 TLS 版本、加密套件、随机数
  2. Server Hello:选定参数、证书(含公钥)、随机数
  3. 客户端验证证书(CA 链、域名、有效期)
  4. 双方用非对称加密协商出对称会话密钥(premaster secret)
  5. 后续 HTTP 数据用对称加密(AES 等)传输

TLS 1.3 改进

  • 握手往返更少(1-RTT,甚至 0-RTT 恢复会话)
  • 禁用弱加密套件,更安全更快

相关概念

  • 对称加密:加解密同一密钥,速度快,传数据用
  • 非对称加密:公钥加密、私钥解密,握手用
  • 证书:CA 对公钥+主体签名,防中间人
  • HSTS:强制浏览器 HTTPS 访问,防降级攻击

HTTP 面试回答模板

问题:HTTP 和 HTTPS 区别

HTTP 是明文应用层协议,默认 80 端口。HTTPS 在 HTTP 与 TCP 之间加入 TLS,加密传输并校验完整性,默认 443,通过证书验证服务器身份,防止窃听和中间人攻击。

问题:GET 和 POST 区别

语义上 GET 安全幂等,用于获取资源;POST 非幂等,用于提交数据。实现上 GET 参数多在 URL,POST 多在 body;GET 可被缓存、收藏;POST 无长度理论限制(看服务器配置)。不应再用「POST 比 GET 安全」概括,敏感数据都应 HTTPS。

问题:HTTP/1.1 和 HTTP/2 区别

1.1 文本协议,持久连接但仍有队头阻塞。2.0 二进制分帧、单连接多路复用多个 Stream 并行,头部 HPACK 压缩,减少延迟。2 仍跑在 TCP 上,丢包会阻塞整连接;3.0 用 QUIC 缓解。

问题:强缓存和协商缓存

强缓存未过期直接用本地,看 Cache-Control max-age。过期后走协商缓存,带 If-None-Match 或 If-Modified-Since 问服务器,未变返回 304 用本地副本,变了返回 200 新内容。ETag 比 Last-Modified 精确。

常见追问

  • TCP 三次握手?SYN → SYN+ACK → ACK,确认双方收发能力。
  • Cookie 和 Session?Cookie 存客户端;Session 存服务端,Cookie 带 SessionId。
  • JWT 和 Session?JWT 无状态、服务端不存;需注意过期与吊销。
  • 502 和 504?502 上游无效响应;504 上游超时。