Skip to content

Java面试小抄

答案为精简版核心要点,建议结合自己的话复述、并用项目经历佐证,而非逐字背诵。⭐ 标注的是各模块中最高频、最容易被深挖追问的核心考点,时间有限时优先保证这些吃透。

一、Java 基础

1. 面向对象三大特性:封装、继承、多态

  • 封装:隐藏内部实现细节,只暴露必要的接口,通过访问修饰符控制。
  • 继承:子类复用父类的属性和方法,支持代码复用,Java 只支持单继承(接口可多实现)。
  • 多态:同一方法在不同对象上表现不同行为,编译时看引用类型,运行时看实际对象类型(动态绑定)。

2. ⭐ == 与 equals() 的区别;为什么重写 equals() 要重写 hashCode()

  • ==:基本类型比较值,引用类型比较内存地址。
  • equals():默认也是比较地址(Object 类),可重写为比较内容(如 String、Integer)。
  • 若重写 equals() 而不重写 hashCode(),会导致两个"相等"的对象哈希值不同,破坏 HashMap/HashSet 等哈希结构的一致性约定(相等的对象必须有相同的 hashCode)。

3. 重载(Overload)与重写(Override)的区别

  • 重载:同一个类中,方法名相同、参数列表不同(类型/个数/顺序),编译期决定,与返回值无关。
  • 重写:子类覆盖父类的同名同参方法,方法签名一致,运行期动态绑定,遵循"两同两小一大"原则(方法名参数相同,返回值和抛出异常范围更小或相等,访问权限更大或相等)。

4. 接口(interface)与抽象类(abstract class)的区别

  • 抽象类可以有构造方法、成员变量、非抽象方法;接口(Java 8+)可以有默认方法、静态方法,但成员变量默认 public static final
  • 类只能单继承抽象类,但可以实现多个接口。
  • 设计理念不同:抽象类是"is-a"关系的模板复用,接口是"can-do"的能力约定。

5. 基本数据类型与包装类型;自动装箱/拆箱原理

  • 基本类型存于栈(局部变量)或对象内部,包装类型是对象,存于堆。
  • 自动装箱调用 valueOf(),自动拆箱调用 xxxValue(),由编译器在编译期插入。
  • 频繁装拆箱有性能损耗,循环中尤其要注意(如 Long 累加)。

6. ⭐ 包装类型的缓存机制(如 Integer 的 -128~127 缓存池)

  • Integer、Short、Byte、Long、Character 对 -128~127 范围内的值会缓存对象(IntegerCache),valueOf() 优先从缓存取。
  • 这导致 Integer a=100,b=100; a==b 为 true,而 Integer a=200,b=200; a==b 为 false(超出缓存范围会 new 新对象)。

7. String、StringBuilder、StringBuffer 的区别

  • String 不可变,每次拼接都会生成新对象。
  • StringBuilder 可变,线程不安全,单线程拼接首选。
  • StringBuffer 可变,方法用 synchronized 修饰,线程安全但性能略低。

8. ⭐ String 为什么是不可变的?不可变有什么好处

  • 底层 value 数组被 final 修饰(JDK 9 前是 char[],JDK 9+ 是 byte[]),且 String 类本身是 final,无法被继承篡改。
  • 好处:线程安全(天然可共享)、可安全用作 HashMap key(hashCode 可缓存)、支持字符串常量池复用节省内存、防止安全漏洞(如作为参数传递时被篡改)。

9. ⭐ String 字符串常量池与 intern() 方法

  • 字符串字面量默认放入常量池(JDK 7+ 位于堆中),相同内容的字面量共享同一对象。
  • new String("a") 会在堆中新建对象,不会自动进入常量池。
  • intern() 方法:如果常量池中已存在相同内容的字符串则返回该引用,否则将当前字符串放入池中并返回。

10. ⭐ 反射机制:原理、应用场景、性能问题

  • 原理:JVM 为每个类生成 Class 对象,通过 Class 对象可以在运行时获取类的结构信息并动态创建对象、调用方法。
  • 应用场景:框架底层(Spring IOC、MyBatis 映射)、注解处理、动态代理。
  • 性能问题:反射调用比直接调用慢(类型检查、方法查找开销),可通过 setAccessible(true) 跳过权限检查、缓存 Method/Field 对象等方式优化。

11. ⭐ 泛型的作用、类型擦除机制

  • 作用:编译期类型检查,避免强制类型转换,提高代码复用性和安全性。
  • 类型擦除:Java 泛型是伪泛型,编译后泛型信息被擦除,替换为原始类型(Object 或上界),因此运行时无法获取泛型的具体类型参数。

12. 注解(Annotation)的原理与应用

  • 本质是接口,继承自 java.lang.annotation.Annotation,通过反射在运行时读取。
  • 元注解 @Retention 控制注解保留策略(SOURCE/CLASS/RUNTIME),只有 RUNTIME 才能被反射读取。
  • 应用:框架配置(如 @Autowired)、代码检查(如 @Override)、代码生成(如 Lombok 编译期处理)。
  • 四个用于修饰自定义注解的元注解:
    • @Target:用于指定被修饰的自定义注解只能用于修饰程序中哪些元素,有四个常用属性(ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER)
    • @Retention:用于指定被修饰的自定义注解可以保留多久,有三个常用属性(SOURCE,CLASS,RUNTIME)
    • @Documented:执行javadoc命令时,被该元注解修饰的自定义注解也会生成在文档中
    • @Inherited:如果父类所使用的注解有此修饰,则子类可以继承该注解,否则不能

13. 异常体系:Exception 与 Error 的区别,Checked 与 Unchecked 异常

  • Throwable 分为 Error(系统级严重错误,如 OOM,不建议捕获)和 Exception(程序可处理的异常)。
  • Checked 异常(编译期强制处理,如 IOException)继承自 Exception 但非 RuntimeException。
  • Unchecked 异常(运行时异常,如 NullPointerException)继承自 RuntimeException,编译器不强制捕获。

14. ⭐ try-catch-finally 执行顺序,finally 中 return 的坑

  • finally 块无论是否抛出异常都会执行(除非 JVM 退出)。
  • 若 try 和 finally 中都有 return,finally 的 return 会覆盖 try 的返回值;若 finally 中修改的是基本类型返回值不会生效(因为 try 的返回值已提前保存),但修改对象属性会生效。

15. 静态变量、静态方法、静态代码块的加载与执行顺序

  • 类加载时按顺序执行:父类静态代码块/变量 → 子类静态代码块/变量 → 父类实例代码块/构造器 → 子类实例代码块/构造器。
  • 静态内容只在类加载时执行一次,实例内容每次 new 对象都执行。

16. 深拷贝与浅拷贝的区别

  • 浅拷贝:只复制对象本身和基本类型字段,引用类型字段仍指向原对象(共享)。
  • 深拷贝:递归复制所有引用类型字段,两个对象完全独立。
  • 实现方式:重写 clone()、序列化反序列化、手动逐字段复制。

17. 序列化与反序列化,transient 关键字作用

  • 序列化:将对象转换为字节流以便存储或网络传输,需实现 Serializable 接口。
  • transient 修饰的字段不会被序列化,反序列化后该字段为默认值。
  • 需注意 serialVersionUID 的作用:版本控制,避免反序列化时类结构不一致报错。

18. Java 8 新特性:Lambda 表达式、Stream API、函数式接口、Optional

  • Lambda:简化匿名内部类写法,用于函数式接口的实例化。
  • Stream API:声明式处理集合数据(filter/map/reduce 等),支持链式调用和并行流。
  • 函数式接口:只有一个抽象方法的接口(如 Runnable、Comparator),可用 @FunctionalInterface 标注。
  • Optional:容器类,用于优雅处理可能为 null 的值,减少 NPE。

19. ⭐ 值传递与引用传递(Java 是否存在引用传递)

  • Java 只有值传递:基本类型传递值的拷贝;对象类型传递的是引用的拷贝(即地址值的拷贝),所以方法内修改对象属性会影响原对象,但重新赋值引用不会影响外部变量。

20. ⭐ JVM 内存区域划分(堆、栈、方法区、程序计数器、本地方法栈)

  • 堆:存放对象实例,GC 主要管理区域,线程共享。
  • 虚拟机栈:每个线程私有,存放栈帧(局部变量表、操作数栈等)。
  • 方法区(元空间):存放类信息、常量、静态变量,JDK 8 后由元空间(本地内存)取代永久代。
  • 程序计数器:记录当前线程执行的字节码行号,线程私有,唯一不会 OOM 的区域。
  • 本地方法栈:为 Native 方法服务。

二、Java 集合

1. 集合框架整体结构:Collection / Map 两大体系

  • Collection 体系:List(有序可重复,如 ArrayList/LinkedList)、Set(无序不可重复,如 HashSet/TreeSet)、Queue(队列,如 LinkedList/PriorityQueue)。
  • Map 体系:键值对存储,如 HashMap、TreeMap、LinkedHashMap,不属于 Collection 接口体系。

2. ⭐ ArrayList 与 LinkedList 的区别及底层实现

  • ArrayList 底层是动态数组,随机访问快(O(1)),中间插入删除慢(需要移动元素,O(n))。
  • LinkedList 底层是双向链表,插入删除快(O(1),已知节点情况下),随机访问慢(O(n),需遍历)。
  • 大多数场景 ArrayList 性能更好(内存连续,缓存友好),LinkedList 适合频繁头尾操作的场景(也可作为双端队列使用)。

3. ⭐ ArrayList 扩容机制(默认容量、扩容倍数)

  • 默认初始容量 10(首次 add 时才真正分配),每次扩容为原容量的 1.5 倍(oldCapacity + oldCapacity >> 1)。
  • 扩容通过 Arrays.copyOf 创建新数组并拷贝数据,属于耗时操作,实际开发中建议预估容量并在构造时指定初始大小。

4. ⭐ HashMap 底层数据结构(数组+链表+红黑树,JDK 8 变化)

  • JDK 7:数组 + 链表,头插法(多线程扩容可能导致环形链表死循环)。
  • JDK 8:数组 + 链表 + 红黑树,尾插法;当链表长度 ≥ 8 且数组长度 ≥ 64 时链表转红黑树,提升查询效率从 O(n) 到 O(log n)。

5. ⭐ HashMap 的扩容机制与 rehash 过程

  • 默认初始容量 16,负载因子 0.75,当元素个数超过"容量 × 负载因子"(阈值)时触发扩容,容量翻倍。
  • JDK 8 优化了 rehash:利用扩容后容量是 2 的幂次特性,节点要么在原位置,要么移动到"原位置+旧容量"处,无需重新计算 hash,只需判断新增的那一位是 0 还是 1。

6. ⭐ HashMap 链表转红黑树的阈值及原因

  • 阈值为 8(TREEIFY_THRESHOLD),且要求数组容量 ≥ 64(MIN_TREEIFY_CAPACITY),否则优先扩容而非树化。
  • 原因:链表过长时查询退化为 O(n),红黑树能保证 O(log n);数组较小时冲突链表变长的概率本就较高,扩容能更有效缓解冲突,避免小容量下过早树化浪费空间。

7. ⭐ HashMap 是否线程安全,为什么;并发场景下如何解决

  • HashMap 非线程安全:多线程并发 put 可能导致数据覆盖丢失;JDK 7 并发扩容可能导致链表成环,引发死循环(CPU 100%);JDK 8 虽解决了成环问题,但仍可能丢数据。
  • 解决方案:使用 ConcurrentHashMap,或用 Collections.synchronizedMap() 包装,或加锁控制。

8. ⭐ HashMap 与 Hashtable、ConcurrentHashMap 的区别

  • HashMap:线程不安全,允许 key/value 为 null。
  • Hashtable:线程安全(方法级 synchronized,锁粒度大,性能差),不允许 null key/value,已过时。
  • ConcurrentHashMap:线程安全,JDK 8 采用 CAS + synchronized(锁粒度细化到桶级别),性能远高于 Hashtable。

9. HashMap 的 hash 算法(扰动函数)原理

  • JDK 8 中 hash = (h = key.hashCode()) ^ (h >>> 16),将高 16 位与低 16 位异或,让 hashCode 的高位也参与运算,减少哈希碰撞,使分布更均匀(数组长度较小时高位信息容易被忽略)。

10. TreeMap 底层实现(红黑树)及排序原理

  • 底层是红黑树,实现了 SortedMap 接口,天然按 key 排序(自然排序或自定义 Comparator)。
  • 插入、删除、查找的时间复杂度均为 O(log n),适合需要有序遍历的场景。

11. ⭐ LinkedHashMap 的实现原理,如何实现 LRU

  • 在 HashMap 基础上通过双向链表维护插入顺序(或访问顺序,需构造时指定 accessOrder=true)。
  • 实现 LRU:继承 LinkedHashMap,设置 accessOrder=true,重写 removeEldestEntry() 方法,当 size 超过容量时自动移除最久未访问的元素。

12. HashSet 底层实现(基于 HashMap)

  • HashSet 内部维护一个 HashMap,元素作为 key 存储,value 固定为一个哑元对象(PRESENT),利用 HashMap key 唯一性保证元素不重复。

13. Vector、Stack 与现代集合的区别及为何不推荐使用

  • Vector:线程安全的动态数组(方法级 synchronized),性能较差,扩容倍数为 2 倍。
  • Stack:继承自 Vector 实现的栈,同样存在性能问题。
  • 现代推荐:ArrayDeque 替代 Stack,ArrayList/CopyOnWriteArrayList 替代 Vector。

14. ⭐ Iterator 遍历原理,fail-fast 与 fail-safe 机制

  • fail-fast:如 ArrayList、HashMap 的迭代器,遍历时通过 modCount 检测集合是否被结构性修改,若被修改(非迭代器自身操作)立即抛出 ConcurrentModificationException。
  • fail-safe:如 CopyOnWriteArrayList,遍历的是底层数组的快照,允许并发修改,但可能读到旧数据(不保证实时性)。

15. Comparable 与 Comparator 的区别

  • Comparable:类内部实现的自然排序接口(compareTo),一个类只能有一种排序方式。
  • Comparator:外部比较器(compare),可以定义多种排序策略,无需修改原类,更灵活(常配合 Lambda 使用)。

16. ⭐ CopyOnWriteArrayList 原理及适用场景

  • 写时复制:每次写操作(add/remove)都会复制一份新数组进行修改,再替换原数组引用,读操作无需加锁。
  • 适用于读多写少的并发场景;缺点是写操作开销大(数组复制)、内存占用高、数据一致性是最终一致(读到的可能是旧快照)。

17. PriorityQueue 底层实现(堆)

  • 底层基于数组实现的二叉堆(默认小顶堆),插入和删除堆顶元素的时间复杂度为 O(log n),可通过 Comparator 自定义排序规则(如实现大顶堆)。

18. Collections 工具类常用方法与线程安全集合的创建

  • 常用方法:sort()、reverse()、shuffle()、max/min()、unmodifiableXxx()(不可变集合)。
  • 线程安全包装:Collections.synchronizedList/Map/Set(),本质是给每个方法加 synchronized 同步锁。

三、并发编程

1. 进程与线程的区别;线程的生命周期状态

  • 进程是资源分配的基本单位,拥有独立内存空间;线程是 CPU 调度的基本单位,共享所属进程的内存空间,创建/切换开销更小。
  • 线程生命周期:新建(New)→ 就绪(Runnable)→ 运行中(Running,Java 中就绪和运行统一为 Runnable)→ 阻塞(Blocked)/等待(Waiting)/超时等待(Timed Waiting)→ 终止(Terminated)。

2. 创建线程的几种方式及各自优缺点

  • 继承 Thread 类:简单但受限于单继承。
  • 实现 Runnable 接口:可继承其他类,推荐方式,任务与线程解耦。
  • 实现 Callable 接口配合 FutureTask:可以有返回值,可抛异常。
  • 线程池创建:推荐的生产方式,统一管理线程资源,避免频繁创建销毁开销。

3. ⭐ synchronized 关键字原理(对象头、Monitor)

  • 每个对象有对象头(Mark Word),记录锁状态信息;synchronized 基于 Monitor(监视器锁)实现,进入同步代码块时 monitorenter,退出时 monitorexit。
  • 方法级 synchronized 通过 ACC_SYNCHRONIZED 标志隐式实现,代码块级通过 monitorenter/monitorexit 字节码指令实现。

4. ⭐ synchronized 锁升级过程:偏向锁→轻量级锁→重量级锁

  • 偏向锁:无竞争时,锁偏向于第一个获取它的线程,避免重复 CAS。
  • 轻量级锁:多个线程交替执行(无实际竞争)时,通过 CAS 自旋获取锁,避免陷入内核态阻塞。
  • 重量级锁:竞争激烈、自旋失败次数过多时膨胀为重量级锁,线程阻塞挂起,涉及用户态和内核态切换,开销最大。
  • 该升级过程不可逆(JDK 6 引入锁升级优化)。

5. ⭐ synchronized 与 ReentrantLock 的区别

  • synchronized 是 JVM 层面的关键字,自动加锁释放锁;ReentrantLock 是 API 层面的类,需手动 lock()/unlock()(务必在 finally 中释放)。
  • ReentrantLock 支持公平锁、可中断锁、超时获取锁、多个 Condition 条件队列,功能更灵活;synchronized 使用更简单,JDK 6 后性能已大幅优化,日常场景两者性能相近。

6. ⭐ volatile 关键字的作用:可见性、有序性,为什么不保证原子性

  • 可见性:保证一个线程修改变量后,其他线程能立即看到最新值(基于 MESI 缓存一致性协议 + 内存屏障)。
  • 有序性:通过插入内存屏障禁止指令重排序,常用于双重检查锁(DCL)单例模式。
  • 不保证原子性:如 i++ 是"读-改-写"三步操作,volatile 只保证每一步的可见性,无法保证整个复合操作不被打断。

7. ⭐ Java 内存模型(JMM):主内存与工作内存

  • JMM 规定所有共享变量存储在主内存,每个线程有自己的工作内存(缓存主内存变量的副本)。
  • 线程对变量的操作都在工作内存中进行,之后再同步回主内存,这种"隔离性"是导致可见性问题的根源,volatile/synchronized/final 等都是用来规范这种交互行为的。

8. ⭐ happens-before 原则

  • JMM 定义的一套规则,用于判断操作之间是否存在可见性保证,无需依赖具体的内存屏障细节。
  • 常见规则:程序次序规则(单线程内代码顺序)、锁规则(解锁 happens-before 后续加锁)、volatile 规则(写 happens-before 后续读)、线程启动/终止规则、传递性规则。

9. ⭐ CAS 原理及 ABA 问题,如何解决

  • CAS(Compare And Swap):比较内存值与预期值,相同则更新为新值,是一种无锁的原子操作,依赖 CPU 指令(如 cmpxchg)。
  • ABA 问题:值从 A 改为 B 又改回 A,CAS 会误判为"未被修改过"。
  • 解决方案:引入版本号/时间戳,如 AtomicStampedReference,每次修改都递增版本号。

10. ⭐ AQS(AbstractQueuedSynchronizer)原理

  • 是构建锁和同步器的框架,内部维护一个 volatile 的 state 变量表示同步状态,以及一个 FIFO 双向队列管理等待线程。
  • 通过 CAS 修改 state 竞争资源,获取失败的线程封装为 Node 加入队列并阻塞(park),释放时唤醒队列中的线程。
  • ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 实现的。

11. ReentrantLock 公平锁与非公平锁

  • 公平锁:严格按照请求锁的顺序(FIFO 队列)分配锁,避免线程饥饿,但吞吐量较低(频繁上下文切换)。
  • 非公平锁(默认):允许"插队",新请求线程可能在队列线程被唤醒前抢到锁,吞吐量更高,但可能导致某些线程长期得不到锁。

12. ⭐ 死锁产生的四个必要条件及如何避免/排查

  • 四个条件:互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。
  • 避免:按固定顺序加锁、使用 tryLock 设置超时、减少锁粒度和持有时间。
  • 排查:使用 jstack 命令查看线程堆栈,会直接标注 "Found one Java-level deadlock"。

13. ⭐ 线程池核心参数(核心线程数、最大线程数、队列、拒绝策略等)

  • corePoolSize:核心线程数,长期驻留(除非设置 allowCoreThreadTimeOut)。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:非核心线程空闲存活时间。
  • unit: keepAliveTime 的时间单位
  • workQueue:任务队列(如 LinkedBlockingQueue、SynchronousQueue)。
  • threadFactory:线程创建工厂。
  • RejectedExecutionHandler:拒绝策略。

14. ⭐ 线程池的工作流程与拒绝策略种类

  • 流程:任务提交 → 核心线程数未满则创建新线程 → 核心线程满则入队列 → 队列满且线程数未达最大值则创建非核心线程 → 都满了则触发拒绝策略。
  • 四种拒绝策略:AbortPolicy(抛异常,默认)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃队列最老任务)。

15. ⭐ 线程池如何合理设置线程数量(CPU 密集型 vs IO 密集型)

  • CPU 密集型:线程数 ≈ CPU 核心数 + 1,减少上下文切换开销。
  • IO 密集型:线程数 ≈ CPU 核心数 × (1 + 平均等待时间/平均计算时间),因为线程大部分时间在等待 IO,可以设置更多线程提高吞吐。
  • 需结合压测实际调整,理论公式仅作为起点参考。

16. Executors 创建线程池方式的弊端,为何阿里规范不推荐

  • Executors.newFixedThreadPool/newCachedThreadPool 等底层使用无界队列(LinkedBlockingQueue)或 Integer.MAX_VALUE 最大线程数,容易导致 OOM 或创建线程数不可控。
  • 阿里规范建议通过 ThreadPoolExecutor 构造函数手动指定参数,明确队列容量和拒绝策略,避免资源耗尽风险。

17. ⭐ ThreadLocal 原理及内存泄漏问题

  • 每个 Thread 内部持有一个 ThreadLocalMap,key 为 ThreadLocal 对象(弱引用),value 为具体值(强引用)。
  • 内存泄漏原因:key 是弱引用,GC 后 key 可能被回收变为 null,但 value 仍被 Entry 强引用,若线程长期存活(如线程池线程),会导致 value 无法回收。
  • 解决:使用完 ThreadLocal 后调用 remove() 方法显式清理。

18. ⭐ CountDownLatch、CyclicBarrier、Semaphore 的区别与使用场景

  • CountDownLatch:一次性计数器,countDown() 递减,await() 等待计数归零,常用于"主线程等待多个子线程完成",用完不能重置。
  • CyclicBarrier:可循环使用的屏障,等待所有线程都到达屏障点后再一起继续执行,支持重置复用。
  • Semaphore:信号量,控制同时访问某资源的线程数量,常用于限流。

19. ⭐ ConcurrentHashMap 的实现原理(JDK 7 分段锁 vs JDK 8 CAS+synchronized)

  • JDK 7:Segment 分段锁,每个 Segment 是一个小 HashMap,锁粒度是段级别,默认 16 段并发度。
  • JDK 8:取消 Segment,直接在数组+链表/红黑树结构上操作,put 时无冲突用 CAS 插入,有冲突时对链表头节点加 synchronized 锁,锁粒度细化到桶级别,并发度大幅提升。

20. 乐观锁与悲观锁的区别及应用场景

  • 悲观锁:认为并发冲突一定会发生,操作前先加锁(如 synchronized、数据库行锁),适合写多读少、竞争激烈的场景。
  • 乐观锁:认为冲突概率低,不加锁,更新时通过版本号或 CAS 检测数据是否被修改过(如数据库 version 字段),适合读多写少场景,冲突高时会有大量重试开销。

21. 原子类(AtomicInteger 等)实现原理

  • 底层基于 CAS + volatile 实现无锁化的原子操作,通过 Unsafe 类调用 CPU 的 CAS 指令,配合自旋重试机制保证操作的原子性,避免使用锁带来的线程阻塞开销。

22. ⭐ Future、CompletableFuture 的使用与原理

  • Future:表示异步计算的结果,可通过 get() 阻塞获取结果,但不支持链式调用和主动通知。
  • CompletableFuture(JDK 8+):实现了 Future 和 CompletionStage,支持链式编排(thenApply/thenCompose/thenCombine)、异常处理(exceptionally)、多任务组合(allOf/anyOf),是异步编程的推荐方案。

23. ⭐ 多线程中 sleep 与 wait 的区别 sleep() 只是让当前线程暂停指定时间且不释放锁,而 wait() 会释放锁并等待被 notify 唤醒,用于线程间协作。

四、MySQL

1. MySQL 基础架构:连接层、SQL 层、存储引擎层

  • 连接层:负责客户端连接、权限验证、连接池管理。
  • Server 层(SQL 层):包含查询缓存(8.0 已移除)、解析器、优化器、执行器,负责 SQL 解析、优化、执行调度。
  • 存储引擎层:负责数据的存储和提取,支持插件式架构(InnoDB、MyISAM 等)。

2. ⭐ 一条 SQL 查询语句是如何执行的

  • 连接器建立连接 → 查询缓存(8.0 前,命中直接返回)→ 解析器做词法/语法分析生成语法树 → 预处理器检查表/字段是否存在 → 优化器生成执行计划(选择索引等)→ 执行器调用存储引擎接口获取数据并返回。

3. ⭐ InnoDB 与 MyISAM 的区别(事务、锁、索引结构)

  • InnoDB:支持事务、外键、行级锁,聚簇索引(数据与索引存放在一起),MySQL 5.5+ 默认引擎。
  • MyISAM:不支持事务,只支持表级锁,非聚簇索引(索引和数据分开存储),查询性能在某些只读场景略快但并发写性能差。

4. ⭐ 事务四大特性 ACID 的实现原理

  • 原子性(Atomicity):通过 undo log 实现,回滚时利用 undo log 恢复数据。
  • 一致性(Consistency):其他三大特性共同保证的最终目标。
  • 隔离性(Isolation):通过锁和 MVCC 实现不同隔离级别。
  • 持久性(Durability):通过 redo log 实现,事务提交时先写 redo log(顺序写磁盘),即使宕机也可通过 redo log 恢复。

5. ⭐ 事务的四种隔离级别及各自解决的问题(脏读/不可重复读/幻读)

  • READ UNCOMMITTED:可能脏读、不可重复读、幻读。
  • READ COMMITTED:解决脏读,仍可能不可重复读、幻读。
  • REPEATABLE READ(MySQL 默认):解决脏读和不可重复读,通过 MVCC + 间隙锁基本解决幻读(当前读场景)。
  • SERIALIZABLE:完全串行化执行,解决所有问题,但并发性能最差。

6. ⭐ MVCC 多版本并发控制原理(Read View、隐藏字段)

  • InnoDB 每行记录隐藏 trx_id(最近修改的事务ID)和 roll_pointer(指向 undo log 的历史版本链)。
  • 通过 Read View(一致性视图)判断当前事务能看到哪个版本的数据,实现"快照读",从而在不加锁的情况下实现读写并发不冲突。

7. ⭐ InnoDB 默认隔离级别为何能避免幻读

  • 快照读场景下依靠 MVCC,同一事务内多次查询使用同一个 Read View,天然不会看到其他事务新插入的数据。
  • 当前读场景(如 SELECT ... FOR UPDATE)下依靠间隙锁(Gap Lock)和临键锁(Next-Key Lock)锁定范围,阻止其他事务在该范围插入新记录。
  • 面试加分点:一定要区分"快照读靠 MVCC 避免幻读,当前读靠间隙锁/临键锁避免幻读"这两种机制不同,面试官很喜欢追问这个区分点。

8. ⭐ 索引的数据结构:为什么用 B+ 树而不是 B 树/红黑树/哈希表

  • B+ 树:非叶子节点只存索引不存数据,叶子节点通过链表相连,适合范围查询和顺序遍历,且相同高度下能存储更多索引项(磁盘 IO 次数更少)。
  • B 树:非叶子节点也存数据,导致单节点能存储的索引项减少,树更高,IO 次数更多。
  • 红黑树:树高随数据量增长较快(本质是平衡二叉树),大数据量下 IO 次数远多于 B+ 树。
  • 哈希表:等值查询快(O(1)),但不支持范围查询和排序。

9. ⭐ 聚簇索引与非聚簇索引(二级索引)的区别

  • 聚簇索引:叶子节点直接存储整行数据,InnoDB 中一张表只有一个聚簇索引(通常是主键,无主键时选唯一非空索引,都没有则隐式生成 rowid)。
  • 非聚簇索引(二级索引):叶子节点存储主键值,查到后需要"回表"根据主键再查一次聚簇索引获取完整数据。

10. ⭐ 联合索引与最左匹配原则

  • 联合索引按照建索引时指定的字段顺序组织排序(先按第一个字段排序,相同时再按第二个字段排序)。
  • 最左匹配原则:查询条件必须从索引最左列开始连续匹配,遇到范围查询(>、<、between、like前缀模糊)会中断后面字段的索引匹配。

11. ⭐ 索引失效的常见场景(模糊查询、函数运算、隐式类型转换等)

  • 索引列上使用函数或运算(如 WHERE YEAR(create_time)=2026)。
  • 模糊查询以 % 开头(如 LIKE '%abc')。
  • 违反最左匹配原则(联合索引跳过前面的列)。
  • 隐式类型转换(如字符串字段用数字查询)。
  • 使用 !=<>OR(部分场景)、NOT IN 等。
  • 优化器判断全表扫描比走索引更快时(如返回结果集占比过大)。

12. ⭐ 覆盖索引、回表查询的概念

  • 回表:通过二级索引查到主键后,再去聚簇索引查完整行数据的过程,涉及两次 B+ 树查找。
  • 覆盖索引:查询所需的所有字段都能在二级索引中直接获取,无需回表,可以显著提升查询性能(EXPLAIN 中 Extra 显示 "Using index")。

13. 索引下推(ICP)的原理

  • MySQL 5.6+ 引入的优化,在联合索引匹配过程中,将 WHERE 条件中可以用索引字段判断的部分下推到存储引擎层过滤,减少回表次数,而不是取回所有数据后在 Server 层过滤。

14. ⭐ 行锁、表锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)

  • 表锁:锁定整张表,开销小但并发度低,MyISAM 只支持表锁。
  • 行锁:锁定具体某行/某几行,InnoDB 默认支持,并发度高但开销大,通过索引实现(无索引会锁全表退化为表锁)。
  • 间隙锁:锁定一个范围(不含记录本身),防止幻读时插入新数据。
  • 临键锁:行锁+间隙锁的结合,锁定一个左开右闭区间,是 InnoDB RR 级别默认的加锁方式。

15. 乐观锁与悲观锁在 MySQL 中如何实现

  • 悲观锁:SELECT ... FOR UPDATE(排他锁)或 LOCK IN SHARE MODE(共享锁),依赖数据库锁机制。
  • 乐观锁:业务层实现,通常为表增加 version 字段,更新时 UPDATE ... SET version=version+1 WHERE id=? AND version=?,通过影响行数判断是否更新成功。

16. 死锁产生原因及如何排查、避免

  • 原因:多个事务以不同顺序竞争多把锁,形成循环等待。
  • 排查:SHOW ENGINE INNODB STATUS 查看最近一次死锁日志,或开启 innodb_print_all_deadlocks
  • 避免:统一加锁顺序、缩小事务粒度和持有时间、为常用查询字段加索引减少锁范围。

17. ⭐ redo log、undo log、binlog 的作用及区别

  • redo log:InnoDB 存储引擎层的物理日志,记录"做了什么修改",用于崩溃恢复,保证持久性,循环写、体积固定。
  • undo log:InnoDB 存储引擎层的逻辑日志,记录修改前的旧值,用于事务回滚和 MVCC 实现一致性读。
  • binlog:MySQL Server 层的逻辑日志,记录所有修改操作,用于主从复制和数据恢复,追加写、不会覆盖。

18. ⭐ 两阶段提交(2PC)如何保证 redo log 与 binlog 一致性

  • prepare 阶段:写 redo log 并标记为 prepare 状态。
  • commit 阶段:写 binlog,成功后再将 redo log 标记为 commit。
  • 若中间崩溃,恢复时会检查 redo log 是否 prepare 但对应 binlog 是否完整,来决定提交还是回滚,从而保证两份日志的数据一致性。

19. binlog 的三种格式(statement、row、mixed)

  • Statement:记录原始 SQL 语句,日志量小,但某些函数(如 NOW())在主从执行结果可能不一致。
  • Row:记录每行数据的具体变更,日志量大,但数据一致性最好,是目前推荐/默认格式。
  • Mixed:MySQL 自动判断,一般语句用 statement,可能引起不一致的语句自动切换为 row。

20. ⭐ 主从复制原理及主从延迟问题

  • 原理:主库将变更写入 binlog,从库 IO 线程拉取 binlog 写入 relay log,从库 SQL 线程重放 relay log 完成同步(也有基于 GTID 的方式)。
  • 延迟原因:主库并发写但从库单线程回放(早期版本)、网络延迟、从库负载高。
  • 优化:并行复制(多线程回放)、半同步复制、读写分离时结合业务容忍延迟或强制读主库。

21. 分库分表的常见方案及路由策略

  • 垂直拆分:按业务模块拆分表/库,解决单表字段过多、业务耦合问题。
  • 水平拆分:按规则(哈希取模、range 范围、一致性哈希)将同一张表的数据拆到多个表/库,解决单表数据量过大问题。
  • 常见中间件:ShardingSphere、MyCat;拆分后需解决跨库 join、分布式事务、全局唯一 ID 等问题。

22. 慢查询定位与 SQL 优化思路(EXPLAIN 使用)

  • 开启慢查询日志(slow_query_log),设置阈值 long_query_time,结合 mysqldumpslow/pt-query-digest 分析。
  • EXPLAIN 关键字段:type(访问类型,如 ALL 全表扫描、index、range、ref、eq_ref、const,性能依次变好)、key(实际使用的索引)、rows(预估扫描行数)、Extra(如 Using filesort、Using temporary 需重点关注)。
  • 优化思路:加合适索引、避免 select *、减少 join 表数量、拆分大事务、合理分页。

23. count(*)、count(1)、count(字段) 的区别

  • count(*):MySQL 做了专门优化,统计所有行数(包含 null),InnoDB 中效率与 count(1) 基本一致。
  • count(1):同样统计所有行数,不取具体字段值。
  • count(字段):会跳过该字段为 null 的行,通常性能略低于前两者(需要判断 null)。
  • 通常推荐直接用 count(*)。

24. 大表 DDL 如何做到不锁表(在线 DDL)

  • MySQL 5.6+ 支持 Online DDL:通过创建临时文件记录 DDL 期间的增量数据变更,原表可继续读写,最后合并数据、原子替换,大幅减少锁表时间。
  • 大表结构变更也可借助 pt-online-schema-change、gh-ost 等第三方工具,进一步降低风险。

25. ⭐ 深分页问题(limit 大偏移量)及优化方案

  • 问题:LIMIT 100000, 10 需要先扫描并丢弃前 100000 行,偏移量越大性能越差。
  • 优化方案:使用"标记上次位置"(如 WHERE id > 上次最大id LIMIT 10)代替 offset;先通过覆盖索引查出主键 id 再回表 join 原表;业务上限制翻页深度。

五、Redis

1. ⭐ Redis 为什么快(内存存储、单线程、IO 多路复用)

  • 数据全部存储在内存中,读写速度远高于磁盘。
  • 单线程模型避免了多线程上下文切换和锁竞争的开销。
  • 采用 IO 多路复用(epoll 等)模型,单线程也能高效处理大量并发连接。
  • 高效的数据结构(如跳表、压缩列表)针对性优化了各种操作的时间复杂度。

2. ⭐ Redis 五种基本数据结构及底层实现(String/Hash/List/Set/ZSet)

  • String:底层是 SDS(简单动态字符串),支持二进制安全,预分配空间减少扩容次数。
  • Hash:小数据量用压缩列表(ziplist/listpack),数据量大或字段多时转为哈希表。
  • List:早期是 ziplist+linkedlist,现统一用 quicklist(双向链表+压缩列表结合)。
  • Set:整数集合用 intset,其他情况用 hashtable。
  • ZSet(有序集合):小数据量用 ziplist,大数据量用跳表(skiplist)+ hashtable 组合。

3. Redis 6.0 后为何引入多线程,多线程用在哪里

  • 网络 IO 的读写(数据的接收和发送)成为高并发场景下的瓶颈,因此引入多线程处理网络数据的读取和写入,但命令执行(真正操作数据)仍是单线程执行,保证了操作的原子性,不需要担心并发安全问题。

4. 跳表(SkipList)在 ZSet 中的应用原理

  • 跳表是一种多层链表结构,通过随机层数实现类似"索引"的加速查找效果,平均时间复杂度 O(log n),实现和维护成本比平衡树更低,且更适合范围查询(如按 score 区间查询)。

5. ⭐ 持久化机制:RDB 与 AOF 的区别、优缺点

  • RDB:某个时间点的内存快照,文件小、恢复速度快,但可能丢失最后一次快照之后的数据,fork 子进程生成快照对内存和 CPU 有瞬时压力。
  • AOF:记录每条写命令,数据更完整(最多丢失 1 秒数据,取决于刷盘策略),但文件体积大、恢复速度相对慢。

6. AOF 重写机制原理

  • AOF 文件会随着运行不断增大,重写(rewrite)时 fork 子进程,根据当前内存中的数据状态直接生成最精简的命令集合(而非重放历史命令),替换旧的 AOF 文件,减小体积。

7. RDB 与 AOF 混合持久化

  • Redis 4.0+ 支持混合持久化:AOF 重写时,将当前内存数据以 RDB 格式写入文件开头,之后的增量命令仍以 AOF 格式追加,既保留了 RDB 恢复快的优点,又保留了 AOF 数据完整性高的优点。

8. Redis 过期删除策略(惰性删除、定期删除)

  • 惰性删除:访问 key 时才检查是否过期,过期则删除,节省 CPU 但可能造成内存中残留大量过期但未被访问的 key。
  • 定期删除:Redis 后台周期性随机抽取部分设置了过期时间的 key 进行检查删除,兼顾内存及时释放和 CPU 开销的平衡。

9. ⭐ 内存淘汰策略(LRU、LFU、随机淘汰等)

  • noeviction:不淘汰,写操作直接报错(默认策略)。
  • volatile/allkeys-lru:淘汰最近最少使用的 key(分别限定在设过期时间/所有key范围内)。
  • volatile/allkeys-lfu:淘汰访问频率最低的 key(4.0+ 引入)。
  • volatile/allkeys-random:随机淘汰。
  • volatile-ttl:优先淘汰剩余存活时间最短的 key。

10. ⭐ 缓存穿透、缓存击穿、缓存雪崩的区别及解决方案

  • 缓存穿透:查询不存在的数据(缓存和数据库都没有),大量请求打到数据库。解决:布隆过滤器提前拦截、缓存空值(设置较短过期时间)。
  • 缓存击穿:某个热点 key 恰好过期,大量并发请求同时打到数据库。解决:热点数据永不过期、互斥锁(只让一个线程重建缓存)。
  • 缓存雪崩:大量 key 同一时间集中过期,或 Redis 整体宕机,导致请求全部压向数据库。解决:过期时间加随机值打散、构建高可用集群、熔断限流兜底。

11. ⭐ 如何保证缓存与数据库的一致性(Cache Aside 模式等)

  • Cache Aside(旁路缓存)模式:读时先读缓存,未命中读数据库并回填缓存;写时先更新数据库,再删除(而非更新)缓存,让下次读时重新加载最新数据,是业界最常用的模式。

12. 双写一致性问题及延迟双删方案

  • 问题:先删缓存再更新数据库,若此时另一线程读取会将旧数据回填缓存,导致缓存脏数据长期存在。
  • 延迟双删:更新数据库前删一次缓存,更新数据库后休眠一小段时间再删一次缓存,尽量覆盖并发读写导致的脏数据窗口,但无法 100% 保证强一致(属于最终一致性方案)。

13. ⭐ Redis 分布式锁的实现(SET NX EX、Redlock)

  • 基础实现:SET key value NX EX 过期时间,保证加锁的原子性;value 建议设置唯一标识(如 UUID),释放锁时用 Lua 脚本先判断 value 再删除,避免误删别人的锁。
  • Redlock:面向多个独立 Redis 节点的算法,需要在多数节点(超过半数)成功加锁才算成功,用于提升高可用场景下锁的可靠性,但社区对其正确性存在一定争议。

14. ⭐ 分布式锁的锁续期问题(看门狗机制)

  • 问题:锁设置了过期时间,但业务逻辑执行时间超过锁过期时间,导致锁提前释放被其他线程抢占。
  • 看门狗(Watch Dog):如 Redisson 实现的后台线程,在持有锁期间定期检测并自动为锁续期,业务执行完毕后停止续期并释放锁。

15. Redis 实现限流的常见算法(令牌桶、滑动窗口)

  • 简单计数器:INCR + EXPIRE 组合实现固定窗口限流,但存在窗口边界突刺问题。
  • 滑动窗口:用 ZSet 记录请求时间戳,每次请求前移除窗口外的旧记录,统计窗口内数量判断是否超限,更平滑。
  • 令牌桶/漏桶:可结合 Redis + Lua 脚本实现,保证限流逻辑的原子性。

16. 主从复制原理及读写分离

  • 全量同步:从库首次连接主库,主库生成 RDB 快照发送给从库,同时缓存复制期间的新写命令,快照加载完后再同步增量命令。
  • 增量同步:基于 replication offset 和 backlog buffer,主库持续将写命令异步发送给从库。
  • 读写分离:主库负责写,从库负责读,分担读压力,但要注意主从延迟导致的短暂数据不一致。

17. 哨兵(Sentinel)机制原理

  • 哨兵是独立进程,负责监控主从节点健康状态、主观下线/客观下线判断(多个哨兵投票达成共识)、自动故障转移(从从库中选出新主库)、通知客户端新的主库地址,实现主从架构的高可用。

18. ⭐ Redis Cluster 集群分片原理(哈希槽 16384)

  • 整个集群划分为 16384 个哈希槽(slot),每个 key 通过 CRC16(key) % 16384 计算所属槽位,槽位分配给不同的主节点负责,实现数据分片和水平扩展。
  • 每个主节点可配置从节点做高可用,节点间通过 Gossip 协议交换集群状态信息。

19. Redis 事务的特性及与关系型数据库事务的区别

  • 通过 MULTI 开启事务,命令入队,EXEC 触发执行;WATCH 命令实现乐观锁(CAS),检测到被监视的 key 在事务提交前被修改则事务失败。
  • 区别:Redis 事务不支持回滚(命令执行报错不会影响其他命令继续执行),仅保证命令的顺序执行和隔离性,不满足严格的 ACID(尤其是原子性)。

20. Pipeline 与事务的区别

  • Pipeline:客户端一次性发送多条命令,减少网络往返(RTT)次数,服务端逐条执行,但不保证原子性,也不保证事务边界。
  • 事务(MULTI/EXEC):保证命令按顺序连续执行,中间不会被其他客户端命令插入,但如前所述不支持真正的回滚。

21. ⭐ Redis 大 Key、热 Key 问题及解决方案

  • 大 Key:单个 key 存储的数据过大(如超大 Hash/List),会导致删除/迁移时阻塞、内存分布不均。解决:拆分为多个小 key、使用异步删除(UNLINK 代替 DEL)。
  • 热 Key:某个 key 访问频率极高,导致单节点负载过高。解决:本地缓存(多级缓存)分担、key 打散为多个副本分散到不同节点。

22. Redis 与本地缓存(如 Caffeine)的多级缓存设计

  • 本地缓存(进程内,如 Caffeine)速度最快但容量有限、多实例间不一致;Redis 作为二级缓存容量大、可共享,但有网络开销。
  • 多级缓存架构:先查本地缓存,未命中查 Redis,再未命中查数据库,逐层回填;需要考虑本地缓存与 Redis 之间的一致性(如通过消息广播失效本地缓存)。

六、Spring / Spring Boot

1. ⭐ IOC(控制反转)与 DI(依赖注入)的原理

  • IOC:将对象的创建和依赖关系的管理从代码中转移给容器(Spring 容器),降低组件间耦合。
  • DI:IOC 的具体实现方式,容器在创建 Bean 时自动将其依赖的其他 Bean 注入进来,常见方式有构造器注入、Setter 注入、字段注入(推荐构造器注入,便于测试和保证不可变性)。

2. ⭐ Spring Bean 的生命周期

  • 实例化(推断构造方法)→ 属性填充(依赖注入)→ 执行 Aware 接口回调(如 BeanNameAware)→ BeanPostProcessor 前置处理 → 执行初始化方法(@PostConstruct / afterPropertiesSet / init-method)→ BeanPostProcessor 后置处理(AOP 代理通常在此生成)→ Bean 可用 → 容器关闭时执行销毁方法(@PreDestroy / destroy-method)。

3. ⭐ Spring 三级缓存解决循环依赖的原理

  • 一级缓存(singletonObjects):完全初始化好的单例 Bean。
  • 二级缓存(earlySingletonObjects):提前暴露的、尚未完成属性填充的半成品 Bean。
  • 三级缓存(singletonFactories):Bean 的 ObjectFactory 工厂,用于生成代理对象(如需要 AOP 代理时延迟到此处生成)。
  • 循环依赖时,A 实例化后提前将自身工厂放入三级缓存,B 依赖 A 时从三级缓存中获取(可能触发 AOP 代理生成)并升级到二级缓存,避免了直接返回原始对象导致的代理不一致问题。

4. Bean 的作用域(singleton、prototype 等)及线程安全问题

  • singleton(默认):容器内唯一实例,全局共享。
  • prototype:每次获取都创建新实例。
  • 还有 request、session、application(需 Web 环境)。
  • 线程安全问题:singleton Bean 若包含可变的成员变量(有状态),会在多线程并发访问时出现数据错乱,应尽量设计成无状态 Bean,或用 ThreadLocal 隔离状态。

5. ⭐ AOP 面向切面编程的原理(动态代理)

  • 将横切关注点(如日志、事务、权限)从业务逻辑中抽离,通过动态代理在方法执行前后织入增强逻辑。
  • 核心概念:切面(Aspect)、切点(Pointcut,匹配规则)、通知(Advice,如 @Before/@After/@Around)、连接点(JoinPoint,可织入的方法)。

6. ⭐ JDK 动态代理与 CGLIB 动态代理的区别

  • JDK 动态代理:基于接口实现(java.lang.reflect.Proxy),目标类必须实现至少一个接口,生成的代理类实现相同接口。
  • CGLIB 动态代理:基于字节码增强(继承目标类生成子类),无需接口,但目标类和方法不能被 final 修饰。
  • Spring 默认:目标类有接口时优先用 JDK 动态代理,可通过配置强制使用 CGLIB。

7. ⭐ Spring 中事务的实现原理(基于 AOP)

  • 通过 AOP 在带有 @Transactional 注解的方法前后织入事务管理逻辑:方法执行前开启事务(获取数据库连接并关闭自动提交),方法正常返回后提交事务,抛出(未被捕获的)运行时异常或指定的检查异常则回滚。

8. ⭐ @Transactional 失效的常见场景

  • 方法内部自调用(this.method(),未经过代理对象,AOP 不生效)。
  • 方法不是 public(Spring 默认只拦截 public 方法)。
  • 异常被 try-catch 捕获且未重新抛出,或抛出的异常类型未在 rollbackFor 中配置(默认只回滚 RuntimeException 和 Error)。
  • 数据库存储引擎不支持事务(如 MyISAM)。
  • 多线程场景(事务与线程绑定,子线程无法感知主线程事务)。

9. ⭐ 事务传播机制(REQUIRED、REQUIRES_NEW 等)

  • REQUIRED(默认):有事务则加入,没有则新建。
  • REQUIRES_NEW:无论是否存在事务,都新建一个独立事务,原事务挂起。
  • NESTED:嵌套事务,基于保存点(Savepoint),子事务可独立回滚不影响外层。
  • SUPPORTS/NOT_SUPPORTED/MANDATORY/NEVER:分别对应"有则用无则不用""强制非事务运行""必须在事务中""禁止事务"等场景,使用频率较低。

10. ⭐ Spring MVC 请求处理流程(DispatcherServlet)

  • 请求先到达前端控制器 DispatcherServlet → 通过 HandlerMapping 找到对应的 Handler(Controller方法)及拦截器链 → HandlerAdapter 执行具体方法 → 方法返回 ModelAndView(或直接返回数据配合 @ResponseBody)→ 视图解析器解析视图(前后端分离场景通常直接序列化 JSON 返回)→ 响应给客户端。

11. Spring 中用到的设计模式(工厂、代理、单例、模板方法、观察者等)

  • 工厂模式:BeanFactory/ApplicationContext 负责创建管理 Bean。
  • 代理模式:AOP 底层实现(JDK/CGLIB 动态代理)。
  • 单例模式:默认作用域的 Bean 管理。
  • 模板方法模式:如 JdbcTemplate、RestTemplate,定义算法骨架,具体步骤交给子类/回调实现。
  • 观察者模式:Spring 事件机制(ApplicationEvent/ApplicationListener)。
  • 策略模式:如 Resource 接口的多种实现。

12. @Autowired 与 @Resource 的区别

  • @Autowired:Spring 自带注解,默认按类型(byType)注入,可配合 @Qualifier 指定具体 Bean 名称按名称注入。
  • @Resource:JDK/JSR-250 提供的注解,默认按名称(byName)注入,找不到再按类型注入,不依赖 Spring,更换框架时可移植性更好。

13. @Component、@Service、@Repository、@Controller 的区别

  • 四者本质都是 @Component 的"派生"注解(元注解都包含 @Component),功能上等价,都会被组件扫描注册为 Bean。
  • 语义区分:@Service 标注业务逻辑层,@Repository 标注数据访问层(会自动转换持久层异常),@Controller 标注控制层,便于代码分层和可读性,部分 AOP 切面也会针对特定注解做定制处理。

14. ⭐ Spring Boot 自动装配(Auto Configuration)原理

  • 通过 @EnableAutoConfiguration(包含在 @SpringBootApplication 中)触发,读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(旧版本是 spring.factories)文件中列出的自动配置类。
  • 每个自动配置类通过 @Conditional 系列注解(如 @ConditionalOnClass、@ConditionalOnMissingBean)判断当前环境是否满足条件,满足则自动注册对应的 Bean,实现"约定优于配置"。

15. @SpringBootApplication 注解的组成

  • 是一个复合注解,主要包含:@SpringBootConfiguration(本质是 @Configuration,标识配置类)、@EnableAutoConfiguration(开启自动装配)、@ComponentScan(默认扫描当前包及子包)。

16. Spring Boot 启动流程

  • 创建 SpringApplication 对象(推断应用类型、加载 ApplicationContextInitializer 和 ApplicationListener)→ run() 方法执行:准备环境(Environment)→ 打印 Banner → 创建 ApplicationContext → 刷新上下文(refresh,完成 Bean 的加载和自动装配,这一步也是 Spring 容器初始化的核心逻辑)→ 执行 CommandLineRunner/ApplicationRunner → 启动完成。

17. Spring Boot 常用 starter 及自定义 starter 的实现思路

  • 常用 starter:spring-boot-starter-web、starter-data-jpa、starter-redis 等,本质是一组依赖 + 自动配置类的集合。
  • 自定义 starter:新建模块,编写自动配置类(用 @Configuration + @ConditionalOnXxx 定义 Bean),在 resources/META-INF/spring 目录下声明该配置类,并配合 @ConfigurationProperties 支持外部化配置,最后打包供其他项目引入依赖即可自动生效。

18. Spring Boot 配置文件加载顺序及多环境配置(profile)

  • 加载顺序(优先级从低到高,后者覆盖前者):application.yml/properties → application-{profile}.yml → 命令行参数/环境变量等外部化配置(外部配置优先级通常更高)。
  • 多环境:通过 spring.profiles.active=dev 激活对应环境的配置文件,实现开发/测试/生产环境的差异化配置隔离。

19. ⭐ Spring 循环依赖:构造器注入为何无法解决

  • 构造器注入要求在实例化阶段就完成依赖注入(对象创建时依赖必须已经就绪),此时三级缓存机制还未介入(Bean 都还没开始实例化,无法提前暴露),因此构造器注入的循环依赖 Spring 无法解决,启动时会直接抛出 BeanCurrentlyInCreationException。

20. Spring Bean 是否线程安全,如何处理有状态 Bean

  • Spring 本身不管理线程安全,取决于 Bean 的作用域和内部实现。singleton 无状态 Bean 天然线程安全;若必须有状态,可改为 prototype 作用域,或用 ThreadLocal 为每个线程保存独立状态,或通过方法参数/局部变量传递状态而非用成员变量。

21. 拦截器(Interceptor)与过滤器(Filter)的区别

  • Filter:Servlet 规范的一部分,运行在 Servlet 容器层,作用范围更广(可处理所有请求,包括静态资源),无法获取 Spring 上下文中的 Bean(需手动获取)。
  • Interceptor:Spring MVC 提供,运行在 DispatcherServlet 之后 Controller 之前,可以直接注入使用 Spring 容器中的 Bean,能获取更细粒度的 Handler 信息(如具体方法)。

22. Spring Boot 常见监控与健康检查(Actuator)

  • Actuator 提供一系列开箱即用的监控端点,如 /actuator/health(健康检查)、/actuator/metrics(性能指标)、/actuator/info(应用信息)、/actuator/env(环境变量),可结合 Prometheus + Grafana 做可视化监控,生产环境需注意做好端点的安全访问控制。

七、MyBatis

1. MyBatis 的工作原理及整体执行流程

  • 加载 mybatis-config.xml 和 Mapper 映射文件,构建 SqlSessionFactory → 通过 SqlSessionFactory 创建 SqlSession → SqlSession 内部通过 Executor 执行器执行 SQL(可选简单执行器/批量执行器/带缓存的执行器)→ 通过 MappedStatement 封装的 SQL 信息,结合 ParameterHandler 处理输入参数、StatementHandler 处理 JDBC Statement、ResultSetHandler 处理返回结果并映射为 Java 对象。

2. ⭐ #{} 与 ${} 的区别(预编译 vs 字符串拼接,SQL 注入风险)

  • #{}:使用 PreparedStatement 预编译参数,以占位符 ? 方式传值,能有效防止 SQL 注入,推荐优先使用。
  • ${}:直接进行字符串拼接替换,存在 SQL 注入风险,通常只用于动态表名、列名等无法用占位符替代的场景,需谨慎并做好参数校验。

3. ⭐ MyBatis 一级缓存与二级缓存的原理及区别

  • 一级缓存:默认开启,作用域是 SqlSession 级别,同一个 SqlSession 内相同查询会直接命中缓存,SqlSession 关闭或执行了增删改操作会清空。
  • 二级缓存:需手动开启(<cache/>),作用域是 Mapper(namespace)级别,可跨 SqlSession 共享,默认使用 PerpetualCache(基于 HashMap),可自定义或整合 Redis 等分布式缓存。

4. 缓存失效的场景及如何禁用缓存

  • 失效场景:执行了任意 INSERT/UPDATE/DELETE 语句会清空对应一级/二级缓存(防止脏读);不同 SqlSession 之间默认不共享一级缓存。
  • 禁用方式:单条语句可设置 flushCache="true"(查询语句默认 false)、useCache="false";全局可在配置文件中关闭二级缓存。

5. ⭐ MyBatis 是否支持延迟加载,实现原理

  • 支持,用于关联查询(如一对多)时按需加载,避免一次性加载全部关联数据造成性能浪费。
  • 原理:基于 CGLIB 或 Javassist 动态代理生成代理对象,只有真正调用到关联属性的 getter 方法时,才会触发对应的 SQL 查询去加载数据。

6. resultMap 与 resultType 的区别

  • resultType:直接指定返回结果映射的 Java 类型,要求数据库列名与 Java 属性名一致(或通过 SQL 别名匹配),适合简单映射。
  • resultMap:可自定义列名与属性名的映射关系,支持处理复杂的关联查询(一对一、一对多)、鉴别器(discriminator)等高级映射场景。

7. 动态 SQL 常用标签(if、choose、foreach、where、set 等)

  • <if>:条件判断,满足条件才拼接对应 SQL 片段。
  • <choose>/<when>/<otherwise>:类似 switch-case,多选一。
  • <foreach>:遍历集合生成批量 SQL(如 IN 条件、批量插入)。
  • <where>:智能处理 WHERE 关键字及多余的 AND/OR。
  • <set>:智能处理 UPDATE 语句中多余的逗号。
  • <trim>:更灵活的自定义前缀/后缀处理逻辑。

8. ⭐ MyBatis 如何实现分页(物理分页 vs 逻辑分页,PageHelper 原理)

  • 逻辑分页(RowBounds):先查出全部数据,再在内存中截取,性能差,不推荐大数据量使用。
  • 物理分页:在 SQL 层面通过 LIMIT(MySQL)等语句只查询需要的数据,性能好,推荐方式。
  • PageHelper 原理:基于 MyBatis 的插件机制(Interceptor),拦截待执行的 SQL,自动在其后拼接对应数据库方言的分页语句(如 LIMIT),并可额外发起一次 COUNT 查询获取总数。

9. 一对一、一对多关联查询的实现方式(嵌套查询/嵌套结果)

  • 嵌套查询(Nested Select):先查主表,再根据外键单独发起子查询获取关联数据,SQL 简单但会产生 N+1 查询问题(可配合延迟加载缓解)。
  • 嵌套结果(Nested Results):通过一次 JOIN 查询获取所有数据,再通过 resultMap 的 <association>(一对一)/<collection>(一对多)标签将结果集手动映射拆分成嵌套对象结构,性能更好但配置相对复杂。

10. Mapper 接口与 XML 是如何绑定并执行的(动态代理)

  • MyBatis 启动时扫描 Mapper 接口,通过 JDK 动态代理为每个接口生成代理对象并注册到 Spring 容器(或 MyBatis 自身的映射注册表)。
  • 调用 Mapper 接口方法时,代理对象的 InvocationHandler(MapperProxy)会根据"接口全限定名+方法名"找到对应的 MappedStatement(即 XML 中或注解中定义的 SQL),交给 SqlSession 执行并返回结果。

11. MyBatis 插件(拦截器)原理,责任链模式的应用

  • MyBatis 允许拦截四大核心对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
  • 通过实现 Interceptor 接口并用 @Intercepts/@Signature 注解声明拦截目标,MyBatis 底层用 JDK 动态代理层层包装这些核心对象,多个插件按配置顺序形成责任链,请求依次经过每个插件的 intercept 方法处理(典型应用如 PageHelper 分页、SQL 性能监控、数据权限过滤)。

12. MyBatis 如何防止 SQL 注入

  • 优先使用 #{} 占位符(PreparedStatement 预编译),避免使用 ${} 直接拼接用户输入。
  • 若必须使用 ${}(如动态表名/排序字段),应在业务层做严格的白名单校验,杜绝直接拼接未经校验的用户输入。

13. MyBatis 批量插入/更新的实现方式及性能优化

  • 通过 <foreach> 标签拼接多组 VALUES 生成一条 INSERT 语句批量插入,比循环单条插入性能高得多。
  • 也可使用 ExecutorType.BATCH 模式的 SqlSession,将多条 SQL 语句缓冲后一次性提交给数据库执行,减少网络往返和事务提交开销;批量数据量过大时建议分批次提交(如每 500~1000 条一批)避免单次事务过大。

14. MyBatis-Plus 相比原生 MyBatis 的增强点

  • 无需编写 XML/SQL 即可实现基础 CRUD(BaseMapper 提供的通用方法)。
  • 内置条件构造器(QueryWrapper/LambdaQueryWrapper),支持链式拼接查询条件,Lambda 方式避免硬编码字段名。
  • 内置分页插件、逻辑删除、乐观锁插件、代码生成器等开箱即用功能,大幅减少样板代码,但复杂 SQL 场景仍建议手写 XML。

15. MyBatis 如何处理数据库与 Java 类型的映射(TypeHandler)

  • TypeHandler 负责 Java 类型与 JDBC 类型之间的相互转换,MyBatis 内置了常见类型的 TypeHandler(如字符串、日期、枚举等)。
  • 对于特殊类型(如将 Java 枚举映射为数据库中的自定义编码、JSON 字段与 Java 对象互转),可自定义实现 TypeHandler 接口(重写 setParameter 和 getResult 方法),并在配置或注解中指定使用。

八、JVM 运行时与垃圾回收

1. ⭐ 类加载机制:双亲委派模型

  • 类加载过程分五步:加载 → 验证 → 准备 → 解析 → 初始化(严格说还有使用、卸载)。
  • 双亲委派:类加载器收到加载请求时,先委托父加载器加载,父加载器无法加载(找不到)时才由自己加载。加载器层级:启动类加载器(Bootstrap)→ 扩展/平台类加载器(Extension/Platform)→ 应用类加载器(Application)→ 自定义加载器。
  • 好处:避免类的重复加载,保证核心类库不被篡改(如自定义 java.lang.String 不会被加载)。
  • 打破双亲委派的场景:JDBC SPI(线程上下文类加载器)、Tomcat(每个 Web 应用独立类加载器实现隔离)、OSGi/热部署框架。

2. 类加载器种类及自定义类加载器

  • BootstrapClassLoader(C++ 实现,加载核心类库)、ExtClassLoader/PlatformClassLoader、AppClassLoader、自定义 ClassLoader(继承 ClassLoader,重写 findClass)。
  • 自定义类加载器常见用途:加密类文件解密加载、模块热替换、多版本类隔离(如 Tomcat 各应用互不干扰)。

3. 对象的内存布局与创建过程

  • 对象头(Mark Word + 类型指针,数组还有长度)+ 实例数据 + 对齐填充(保证 8 字节对齐)。传统 64 位 JVM 中对象头占 12~16 字节。
  • 创建流程:检查类是否已加载解析初始化 → 分配内存(指针碰撞/空闲列表,根据是否有 TLAB 决定是否加锁)→ 初始化零值 → 设置对象头 → 执行构造方法 <init>
  • TLAB(线程本地分配缓冲):为每个线程预分配一块私有内存,减少多线程分配对象时的同步开销。
  • 【经核实的2025年新特性,可作为加分项提及】紧凑对象头(Compact Object Headers,JEP 519):JDK 25(当前最新 LTS,2025年9月发布)中转正为正式特性,将 Mark Word 与类指针合并压缩为 64 位(8 字节),对象头从 12~16 字节压缩到 8 字节,官方测算可减少约 30% 内存占用、降低 GC 压力。注意:该特性目前默认不开启,需手动加 -XX:+UseCompactObjectHeaders 参数。

4. ⭐ 分代收集理论与内存区域

  • 弱分代假说:绝大多数对象朝生夕灭。因此堆划分为新生代(Eden + 2 个 Survivor,默认 8:1:1)和老年代。
  • Minor GC:回收新生代,触发频繁但耗时短;Major GC/Full GC:回收老年代(或整堆),触发频率低但耗时长,影响大。
  • 对象晋升老年代的条件:年龄超过阈值(默认 15,-XX:MaxTenuringThreshold)、大对象直接进老年代(-XX:PretenureSizeThreshold)、Survivor 空间不够时提前晋升(动态年龄判定)。

5. ⭐ 常见垃圾回收器对比(重点:G1 原理,面试必考)

  • Serial/Serial Old:单线程,简单高效,适合客户端/小内存场景。
  • Parallel Scavenge/Parallel Old:多线程,吞吐量优先,JDK 8 默认组合。
  • CMS:老年代并发收集,标记-清除,追求最短停顿,但有内存碎片和并发失败(Concurrent Mode Failure)问题,JDK 9 起弃用。
  • G1(Garbage First):JDK 9+ 默认收集器。将堆划分为多个大小相等的 Region(新生代/老年代不再物理隔离),基于"停顿预测模型"优先回收垃圾最多的 Region;用 Remembered Set 记录跨 Region 引用,避免全堆扫描;可设置期望停顿时间(-XX:MaxGCPauseMillis)。
  • ZGC/Shenandoah:JDK 11/12+ 引入的低延迟收集器,通过着色指针(Colored Pointer)+ 读屏障实现并发标记整理,停顿时间可控制在毫秒级,几乎与堆大小无关,适合超大堆场景。

6. ⭐ GC 判断对象存活:可达性分析与引用类型

  • 可达性分析:从 GC Roots(栈中引用、静态变量、常量、JNI 引用等)出发,不可达的对象判定为可回收。
  • 四种引用:强引用(正常引用,永不回收)、软引用(内存不足才回收,SoftReference,适合做缓存)、弱引用(下次 GC 必回收,WeakReference,如 ThreadLocalMap 的 key)、虚引用(PhantomReference,仅用于跟踪对象被回收的状态,需配合引用队列)。

7. ⭐ 内存溢出(OOM)与内存泄漏排查思路

  • 常见 OOM 类型:堆溢出(java.lang.OutOfMemoryError: Java heap space)、栈溢出(StackOverflowError,通常是递归过深)、元空间溢出(大量动态生成类,如 CGLIB 代理未复用)、直接内存溢出(NIO 堆外内存未释放)。
  • 排查工具链:jps 找进程 → jstat 看 GC 频率/内存占用趋势 → jmap -dump 导出堆快照 → MAT(Eclipse Memory Analyzer)分析支配树找到内存泄漏根因 → jstack 分析线程堆栈(死锁/死循环)。
  • 生产环境更推荐用阿里开源的 Arthas:在线诊断,无需重启,支持 thread(查线程)、jad(反编译)、watch(观察方法出入参)、trace(方法链路耗时)等命令。

8. ⭐ JVM 调优的一般思路

  • 明确目标:是要降低停顿时间(延迟敏感)还是提升吞吐量(离线计算类)。
  • 调优步骤:先监控(GC 日志 -Xlog:gc*、Arthas、Prometheus+Grafana)定位问题 → 分析是频繁 Full GC、内存泄漏还是线程阻塞 → 针对性调整堆大小/新生代比例/收集器类型 → 压测验证效果,避免过度调优。
  • 常见误区:盲目调大堆内存(可能导致单次 GC 停顿更久)、迷信某个收集器(应结合业务场景和堆大小选择)。

九、网络与 IO 模型

1. ⭐ TCP 三次握手、四次挥手

  • 三次握手:客户端 SYN → 服务端 SYN+ACK → 客户端 ACK,目的是确认双方的收发能力都正常,防止历史失效的连接请求突然又传到服务端造成资源浪费。
  • 四次挥手:主动方 FIN → 被动方 ACK → 被动方 FIN → 主动方 ACK。之所以是四次而非三次,是因为被动方收到 FIN 时可能还有数据未发送完,ACK 和 FIN 不能合并发送。
  • TIME_WAIT 状态:主动关闭方需等待 2MSL,确保最后的 ACK 能被对方收到,同时让网络中滞留的旧报文失效。

2. ⭐ TCP 与 UDP 的区别,如何保证 TCP 可靠传输

  • TCP:面向连接、可靠、有序,通过序号确认、超时重传、滑动窗口(流量控制)、拥塞控制(慢启动/拥塞避免/快重传/快恢复)实现可靠传输。
  • UDP:无连接、不可靠、无序,但开销小、实时性好,适合视频/直播/DNS 等场景。

3. ⭐ BIO / NIO / AIO 的区别

  • BIO(阻塞 IO):一个连接一个线程,线程在读写时阻塞,高并发下线程资源耗尽。
  • NIO(非阻塞 IO):基于 Channel + Buffer + Selector,一个线程通过多路复用监听多个 Channel 的就绪事件,是 Redis/Netty 底层模型的基础。
  • AIO(异步 IO):真正的异步,发起 IO 请求后立即返回,由操作系统完成后回调通知,Linux 下支持有限,实际生产用得较少。

4. Netty 的核心组件及为什么用 Netty 而不是原生 NIO

  • 核心组件:EventLoop(事件循环,一个线程处理多个 Channel)、Channel(连接抽象)、ChannelPipeline(处理器链)、ByteBuf(更高效的缓冲区,替代 NIO 原生 ByteBuffer)。
  • 原生 NIO 的痛点:API 复杂、空轮询 Bug(Selector 空转导致 CPU 100%)、需要自己处理粘包拆包,Netty 都做了封装和修复,是目前 Java 高性能网络编程的事实标准。

5. 粘包与拆包问题及解决方案

  • 成因:TCP 是流式协议,没有消息边界,发送方多次 write 的数据可能被合并或拆分接收。
  • 解决方案:固定长度报文、分隔符(如 \n)、在消息头部加长度字段(LengthFieldBasedFrameDecoder,Netty 内置,最常用)。

十、分布式系统与消息队列

1. ⭐ 分布式事务解决方案(2PC、3PC、TCC、Saga、本地消息表)

  • 2PC(两阶段提交):协调者询问所有参与者是否可以提交(prepare),全部同意后再统一 commit,强一致但同步阻塞、单点故障风险高。
  • TCC(Try-Confirm-Cancel):业务层面实现的柔性事务,Try 阶段预留资源,Confirm 确认执行,Cancel 回滚,性能好但业务侵入性强,需要保证幂等。
  • 本地消息表 / 事务消息:业务操作和消息记录在同一本地事务中,再通过定时任务/MQ 事务消息异步投递,实现最终一致性,是电商场景(如下单扣库存)最常用的方案。
  • Saga:将长事务拆分为多个本地事务,每步都有对应的补偿操作,适合业务链路长的场景(如出行、旅游订单)。

2. ⭐ 分布式 ID 生成方案

  • UUID:简单但无序,作为数据库主键会导致 B+树频繁分裂,性能差。
  • 数据库自增/号段模式:预分配一批 ID 到内存,用完再申请下一批,减少数据库访问。
  • 雪花算法(Snowflake):64 位 = 1 位符号位 + 41 位时间戳 + 10 位机器标识 + 12 位序列号,趋势递增、性能高,但依赖机器时钟,存在时钟回拨问题(需结合等待或拒绝策略处理)。
  • Redis INCR / Leaf(美团开源)等也是常见方案。

3. ⭐ 消息队列如何保证消息不丢失(生产、存储、消费三个环节)

  • 生产端:开启 confirm 确认机制(RocketMQ 同步发送/Kafka acks=all),发送失败重试。
  • 存储端:消息持久化到磁盘,Kafka 通过多副本(Leader+Follower)+ ISR 机制保证数据不丢;开启同步刷盘可靠性更高但性能下降。
  • 消费端:消费成功后再手动提交 offset(关闭自动提交),避免"取到消息但处理失败却已确认"的丢失场景。

4. ⭐ 消息队列如何保证消费幂等性

  • 消息可能因网络重试、rebalance 等原因被重复投递,消费端必须保证幂等:业务上唯一键 + 数据库唯一索引拦截重复插入、Redis SETNX 记录已处理的消息 ID、状态机判断(如订单状态已是"已支付"则跳过)。

5. 消息队列如何保证消费顺序性

  • 全局有序代价很高,一般只需局部有序:将有序性要求的消息(如同一订单的多个状态变更)通过相同的 key/分区键(Kafka Partition Key / RocketMQ 顺序消息的 ShardingKey)路由到同一个分区,由单一消费者顺序消费。

6. Kafka 与 RocketMQ 的核心概念对比

  • Kafka:Topic → Partition → Segment,高吞吐(顺序写 + 零拷贝 + 批量发送),偏重日志采集/大数据场景,事务消息支持相对较弱。
  • RocketMQ:Topic → Queue,阿里开源,原生支持事务消息、延迟消息、顺序消息,金融/电商场景更常用。

7. ⭐ CAP 定理与 BASE 理论

  • CAP:一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者在分布式系统中最多同时满足两个,网络分区不可避免,因此实际是在 C 和 A 之间权衡。
  • BASE:基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventually Consistent),是对 CAP 中一致性妥协后的实践理论,绝大多数互联网系统采用 BASE 而非强一致。

十一、微服务与高可用

1. 服务注册与发现的作用(Nacos/Eureka/ZooKeeper)

  • 服务提供者启动时向注册中心注册自身地址,消费者从注册中心订阅并拉取可用实例列表,配合心跳机制剔除失效节点,解决了微服务架构下服务实例动态变化、无法硬编码地址调用的问题。

2. ⭐ 服务熔断、降级、限流的区别(Sentinel/Hystrix)

  • 限流:主动控制流量上限,超过阈值直接拒绝,防止系统被压垮(常见算法:令牌桶、漏桶、滑动窗口)。
  • 熔断:类似电路的保险丝,当下游服务错误率超过阈值时,自动切断调用一段时间(避免持续请求已经故障的服务,防止雪崩),期间直接走降级逻辑,之后进入半开状态试探恢复情况。
  • 降级:当核心资源不足或依赖故障时,牺牲非核心功能(如返回兜底数据/缓存旧数据),保证核心链路可用。

3. API 网关的作用

  • 统一入口,负责路由转发、鉴权、限流、日志、协议转换等横切关注点,避免每个微服务重复实现这些能力,常见实现:Spring Cloud Gateway、Nginx+Lua(OpenResty)。

4. 分布式session一致性问题的常见方案

  • Session 复制(成本高,不推荐)、Session 粘滞(Nginx ip_hash,单点故障时该用户 session 丢失)、集中式存储(Redis 存储 Session,推荐方案)、无状态化改造(JWT Token,服务端不存状态,信息都放在 Token 中签名校验)。

十二、设计模式高频

1. ⭐ 单例模式的几种实现方式及优劣

  • 饿汉式:类加载时直接创建,线程安全但可能浪费内存(无论是否使用都会创建)。
  • 懒汉式 + synchronized:线程安全但每次获取都要加锁,性能差。
  • 双重检查锁(DCL):两次判空 + synchronized 代码块,需要给实例变量加 volatile 防止指令重排序导致其他线程拿到未初始化完成的对象。
  • 静态内部类:利用类加载机制保证线程安全,且是懒加载,是最推荐的写法之一。
  • 枚举单例:《Effective Java》推荐写法,天然防止反射和序列化破坏单例。

2. 工厂模式(简单工厂/工厂方法/抽象工厂)

  • 简单工厂:一个工厂类根据参数创建不同产品,新增产品需修改工厂类,违反开闭原则。
  • 工厂方法:每种产品对应一个工厂类,符合开闭原则,但类数量会膨胀。
  • 抽象工厂:生产一族相关产品(如不同操作系统下的按钮+文本框组合),扩展新产品族容易,扩展新产品种类难。

3. 观察者模式与发布订阅模式

  • 观察者模式:主题(被观察者)维护观察者列表,状态变化时主动通知所有观察者,观察者和主题直接耦合(如 Spring 的 ApplicationEvent)。
  • 发布订阅模式:引入中间的消息代理(Broker),发布者和订阅者互不感知,进一步解耦(如 MQ)。

4. 装饰器模式与代理模式的区别

  • 装饰器:为对象动态添加职责,强调功能增强,被装饰对象和装饰器通常实现同一接口,可以多层嵌套叠加(如 Java IO 流的 BufferedInputStream 包装 FileInputStream)。
  • 代理:控制对目标对象的访问,强调职责不变但加控制逻辑(如权限校验、延迟加载),通常一层代理。

5. 责任链模式的应用场景

  • 请求沿着一条处理者链传递,每个处理者判断是否处理、是否继续向下传递,典型应用:Servlet Filter、MyBatis 插件链、Netty ChannelPipeline、Spring Security 过滤器链。

十三、场景设计题(中高级常问,需结合项目经验作答)

  • ⭐ 秒杀系统如何设计? 核心思路:前端限流(按钮防抖、验证码)→ 网关层限流 → 库存预热到 Redis,用 Lua 脚本保证扣减原子性 → 请求异步化,下单请求先入 MQ 削峰,再由后端异步处理生成订单 → 数据库层面最终对账兜底,防止超卖(可加数据库乐观锁二次校验)。
  • ⭐ 如何设计一个高并发下的分布式限流方案? 单机限流(Guava RateLimiter)在分布式场景下会导致总限流数超预期,需要用 Redis + Lua 脚本实现集中式限流计数,或用 Sentinel 集群限流模式,由 Token Server 统一发放令牌。
  • ⭐ 线上接口突然变慢,如何排查? 先看监控大盘定位是否全局问题还是局部接口 → 查看该实例 CPU/内存/GC 情况(是否 Full GC 频繁)→ 用 Arthas trace 命令定位链路中最耗时的方法 → 检查是否有慢 SQL、外部依赖超时、线程池队列堆积等。

最后更新于: