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、外部依赖超时、线程池队列堆积等。