ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java面试核心考点全解析:集合、并发、JVM与分布式锁

Java面试核心考点全解析:集合、并发、JVM与分布式锁 一年两度的 Java 面试季又来了后台不断有人问同一个问题java面试题到底应该怎么刷我这些年线上线下看了不下几百份简历也面过不少候选人发现翻车的人大多栽在同一个地方——题背得很熟面试官一追问就露馅。这份“JAVA最全面试题大全二”跟市面上的题库不一样我不会丢一堆题和答案让你死记而是把核心考点拆开揉碎讲清楚每道题背后的考察意图、回答逻辑和常见的坑。这篇承接第一部分的 Java 基础语法内容聚焦面试里真正拉分的地方集合框架、并发编程、JVM、Spring 与 MyBatis、MySQL 索引、Redis 缓存、分布式锁这几大板块。每一道题我都会按照面试官的真实追问逻辑往下带告诉你哪些话该说、哪些话说了反而减分。适合正在准备 Java后端开发岗位面试的人也适合工作两三年想系统查漏补缺的工程师。它可以直接当复习提纲用也可以当作模拟面试的问答脚本。保质期很长建议收藏。1. 这份题库的打开方式别一上来就背答案1.1 为什么题背熟了照样挂我在模拟面试中遇到过很多候选人八股文背得滚瓜烂熟比如问 JVM 内存区域能把堆、栈、方法区一字不差说出来再追问一句“你线上调过 JVM 参数吗”立刻就沉默了。这种“背题式复习”最大的问题在于面试官要的不是复读机而是能解决问题的工程师。面试题的本质是一种交流。面试官抛出一个问题真正想看的是你面对未知问题的思维过程、知识体系的完整度、以及能否把理论落到工程实践。所以你会发现大厂面试很少只问单一知识点而是从一个点不断往外延展。比如问你 HashMap可以从数据结构问到哈希冲突再问到扩容再问到并发安全性最后抛给你一个场景让你选容器。整个过程就像剥洋葱一层一层往下挖。这也是我整理这份题库的核心思路每个题目我会写出高频追问路径你把每个环节都理解了而不是只背标准答案。答题的时候有一个小原则——能用一句话讲清楚原理的就不要绕圈子能举例子说明的就不要只讲概念能带上踩坑经验的那就是加分项。1.2 准备面试题的正确姿势四段式答题法我自己筛候选人时最喜欢听到的回答结构是四段式是什么、为什么、怎么用、有什么坑。这四步恰好覆盖了面试官想考察的四个维度——知识广度、原理深度、工程能力、经验沉淀。拿“JDK 动态代理和 CGLIB 有什么区别”来举例。第一段“是什么”JDK 动态代理基于接口用 Proxy.newProxyInstance 生成代理对象CGLIB 基于继承通过生成子类来实现代理。第二段“为什么”因为 Java 是单继承模型JDK 代理只能代理接口CGLIB 通过字节码技术生成子类所以可以代理类。第三段“怎么用”Spring AOP 里默认策略是有接口就 JDK没有接口就 CGLIBSpring Boot 2.x 之后默认强制 CGLIB。第四段“有什么坑”JDK7 之前 CGLIB 生成的代理类在 PermGenJDK8 之后在 Metaspace目标类如果被 final 修饰CGLIB 无法代理Spring Boot 2.x 会直接报错。这套结构练熟了好处非常明显。首先你不会冷场即使追问超出准备范围也能顺着结构往下说。其次面试官会觉得你有工程思维不是背题的料。后面章节所有题目我都建议套用这个结构去消化。2. Java 语言核心最基础的题往往最见功底2.1 String、StringBuilder、StringBuffer怎么答才不浅这道题几乎是 Java 面试的必考题出现概率超过八成。很多人会快速回答String 不可变StringBuilder 线程不安全StringBuffer 线程安全方法加 synchronized。这个答案能拿基础分但拿不到高分。面试官真正想听的是底层原理和场景判断。String 不可变是因为内部用 final char[]JDK9 之后是 byte[]存储并且类本身被 final 修饰。不可变带来的好处有三个字符串常量池缓存安全、hash 值只需要计算一次、多线程天然安全。StringBuilder 可变且不保证线程安全所以单线程拼接性能最好StringBuffer 每个方法都加锁性能最差但线程安全。接着面试官大概率会追问字符串拼接用 和 StringBuilder 有什么区别这里有个重要细节Java 编译器对 拼接会在编译期优化为 StringBuilder 的 append 调用。但是在循环体内拼接每次循环都会 new 一个 StringBuilder性能反而会变差所以循环内手动用 StringBuilder 更合理。我建议把这个点主动说出来面试官会觉得你关注过字节码层面的优化。还有一个高频变形题String s new String(abc) 创建了几个对象如果常量池里没有 “abc”会在常量池创建 1 个对象在堆里 new 1 个对象共 2 个如果常量池已存在就只在堆里创建 1 个。这里踩坑的点在于很多人会把常量池和堆里的对象搞混回答的关键是强调“常量池里不存在时才会额外创建”。2.2 Integer 缓存与自动拆装箱的连环追问基础题里最容易出现“答案对但解释错”的就是 Integer 缓存。典型问题Integer a 127Integer b 127a b 是 true 还是 false如果换成 128 呢答案是 127 时为 true128 时为 false。原因是 Integer 在 -128 到 127 范围内使用缓存对象valueOf 方法会直接返回缓存中的对象超出范围则 new 新对象。这里我建议主动把源码逻辑说出来IntegerCache.low 是 -128high 默认是 127high 可以通过 -XX:AutoBoxCacheMax 参数调整。能说到这个参数说明你确实看过源码。接着会追问自动拆装箱。Integer c 100 实际执行的是 Integer.valueOf(100)int d c 实际执行的是 c.intValue()。如果考得更细会让你判断这段代码有没有问题Integer x 1000; Integer y 1000; System.out.println(x y); // false System.out.println(x.equals(y)); // true很多人会纠结 和 equals其实核心就一句话包装类型比较值必须用 equals用 比较的是引用地址。还有一个常见的性能坑在循环里大量做 int 和 Integer 的自动装箱会产生大量临时对象触发 GC 压力。这些点如果能在回答里带出来这道基础题你的得分一定不低。2.3 接口、抽象类、final以及异常体系常考点接口和抽象类的区别是另一个必问项。常规回答是抽象类用 abstract class 定义可以有构造方法、成员变量、普通方法接口用 interface 定义JDK8 之后可以有 default 和 static 方法JDK9 之后可以有 private 方法。抽象类是“is-a”关系接口是“can-do”能力契约。面试官可能追问为什么接口里不能有实例字段这个问题要回到设计初衷接口定义的是对外行为规范实例字段会暴露内部状态破坏封装性。再追问JDK8 为什么引入 default 方法因为要给 List、Map 等接口新增方法如 stream、forEach又不想让所有实现类都改代码default 方法提供的是向后兼容的方案。final 关键字也是个考点。final 修饰类不可被继承、修饰方法不可被重写、修饰变量不可被修改。值得主动补充的是 final 与并发的关系final 字段在构造器中赋值后其他线程无需同步就能看到正确的值这涉及 JMM 的 final 语义。异常体系同样常考。需要把 Throwable 作为根节点下面分 Error 和 Exception 这条线讲清楚Exception 再分受检异常和运行时异常。典型受检异常是 IOException、SQLException典型运行时异常是 NullPointerException、IllegalArgumentException。高频追问是业务代码里什么时候抛受检异常什么时候抛运行时异常我的经验是可恢复的、调用方能处理的用受检异常编程错误和不可恢复情况用运行时异常代码更干净。3. 集合框架HashMap 一题能问半小时3.1 HashMap 底层结构与扩容机制HashMap 是 Java 面试的顶流没有之一。问法非常集中底层结构是什么在 JDK7 和 JDK8 有什么区别JDK7 的 HashMap 是数组加链表新元素采用头插法JDK8 改成数组加链表加红黑树采用尾插法。头插法在并发扩容时会形成环形链表导致 get 死循环这是 JDK7 的经典 bugJDK8 改尾插也是为了规避这个问题。不过要注意JDK8 的 HashMap 依然不是线程安全的并发下丢数据、size 不准确的问题仍然存在。扩容机制需要讲清楚三个数字默认初始容量 16负载因子 0.75扩容阈值是容量乘以负载因子。每次扩容翻倍到 32、64。容量必须是 2 的幂次原因是计算桶下标时用 hash (n-1) 代替取模运算位运算性能更高并且容量是 2 的幂时结果分布更均匀。讲到这里面试官通常满意。3.2 为什么用红黑树、为什么树化阈值是 8这个问题很多人接不住。先说结论链表长度超过 8 且数组长度大于等于 64 时链表变成红黑树。红黑树是近似平衡的二叉搜索树增删查都是 O(logn)而链表最坏是 O(n)。当哈希碰撞严重时红黑树能把查询性能从 O(n) 降到 O(logn)。为什么要设 8 这个阈值这个数字不是拍脑袋定的。HashMap 源码注释里有详细说明基于泊松分布的计算负载因子 0.75 下链表长度到达 8 的概率约是千万分之六也就是说正常情况几乎不会触发树化。树化是应对极端恶意 hash 碰撞的一种防御机制。还有一个配套考点树退化。红黑树中元素数量降到 6 时会转回链表。这里不要答成降到 8源码里 TREEIFY_THRESHOLD 是树化阈值 8UNTREEIFY_THRESHOLD 是退化阈值 6。中间留了 7 的缓冲是为了避免元素在 7、8 之间频繁插入删除导致反复树化和退化消耗性能。3.3 ConcurrentHashMap 的线程安全实现HashMap 线程不安全已经确认了那并发场景用什么答案就是 ConcurrentHashMap。如果你能讲清楚它不同版本的演进这道题直接封神。JDK7 的 ConcurrentHashMap 采用分段锁默认分成 16 个 Segment每个 Segment 是一把独立的 ReentrantLock操作时只锁当前分段并发度是 16。JDK8 放弃了分段锁改用 CAS 加 synchronized锁的粒度细化到桶的首节点。put 时先用 CAS 尝试插入失败再 synchronized 锁住首节点。这样并发度不受分段数量限制锁更细性能更好。size() 方法也值得提一下。JDK8 的 size() 不是简单遍历所有节点而是先尝试无锁统计 baseCount如果竞争激烈再用 CounterCell 数组分片记录增减量最后累加。这套设计很有意思跟 LongAdder 的思路完全一致面试官问到 LongAdder 时也可以关联上。3.4 ArrayList、LinkedList 与 fail-fast这两者的对比是高频热身题。ArrayList 底层是动态数组查询快按下标访问 O(1)插入删除要移动元素O(n)。LinkedList 底层是双向链表插入删除在已知节点时很快但按下标访问需要遍历O(n)。扩容方面ArrayList 每次扩容为原来的 1.5 倍JDK8 里通过 Arrays.copyOf 实现。真正的加分点是 fail-fast 机制。ArrayList、HashMap 的迭代器是 fail-fast 的迭代过程中检测到 modCount 发生变化立刻抛出 ConcurrentModificationException。很多人只知道现象我会建议补一句modCount 是结构性修改次数迭代器创建时会保存期望值每次 next 都会检查如果集合被修改了这个期望值和 modCount 不一致就抛异常。fail-fast 只是快速失败不保证正确性并发修改的正确方案是使用并发容器。这里可以再延伸一个点哪些容器是 fail-safe 的CopyOnWriteArrayList、ConcurrentHashMap 的迭代器基于快照遍历时不会抛 ConcurrentModificationException但同样不保证实时一致性。这一段说出来集合这块就稳了。4. 并发编程线程池、锁与可见性4.1 线程池的参数与执行流程线程池是并发面试的重灾区也是工作中真正高频使用的工具。第一个问题通常是线程池有哪些核心参数我建议背到反射级别七个参数一个都不能漏corePoolSize 核心线程数、maximumPoolSize 最大线程数、keepAliveTime 空闲存活时间、unit 时间单位、workQueue 任务队列、threadFactory 线程工厂、handler 拒绝策略。执行流程更关键按顺序讲清楚提交任务后先判断核心线程是否已满不满就创建核心线程执行满了就放入队列队列满了再判断最大线程数没满就创建非核心线程执行都满了就触发拒绝策略。这里我经常用生活化类比帮助理解核心线程是正式员工队列是待办清单非核心线程是外包拒绝策略就是实在接不下活的应对方式。拒绝策略有四种AbortPolicy 直接抛异常、CallerRunsPolicy 由提交任务的线程自己执行、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢弃队列中最老的任务。我在实际项目里推荐 CallerRunsPolicy它不会丢任务还能通过提交线程反向施加背压让系统放慢生产速度。最大的坑在 Executors 创建线程池。我强烈建议不要在线上用 Executors.newFixedThreadPool 或 newCachedThreadPool。前者队列是无界 LinkedBlockingQueue任务堆积会把内存打爆后者最大线程数是 Integer.MAX_VALUE极端情况会创建海量线程直接把 CPU 和内存耗尽。正确做法是用 ThreadPoolExecutor 显式构造队列用有界队列实际容量根据业务评估。4.2 synchronized 与 ReentrantLock 怎么选这个问题考察你对锁的理解深度。synchronized 是 JVM 层面通过 monitor 实现的ReentrantLock 是 JDK 提供基于 AQS 实现的。JDK6 之后 synchronized 引入锁升级机制无锁到偏向锁偏向锁到轻量级锁轻量级锁再到重量级锁。锁只能升级不能降级。高并发场景下偏向锁基本没有意义JDK15 之后默认就废弃了偏向锁。ReentrantLock 的优势要讲清楚四点支持中断响应lockInterruptibly、支持超时获取tryLock、支持公平锁、支持多个 Condition 条件队列。日常开发里synchronized 足够用需要尝试获取锁、超时控制、或者精细的等待唤醒控制时选 ReentrantLock。这里有一个性能误区要澄清低竞争下 synchronized 和 ReentrantLock 性能差距很小不要盲目迷信某一种。可以加一段自旋的内容。锁竞争时线程不是立即挂起而是执行几个空循环尝试获取锁这就是自旋。自旋能避免线程切换的开销但会占用 CPU。JDK 默认开启自适应自旋JVM 会根据上次自旋结果动态调整自旋次数。4.3 volatile、ThreadLocal 与 CASvolatile 是并发基础题里最容易讲不清的。它的语义只有两条可见性和有序性。线程修改 volatile 变量后其他线程立即可见JVM 会在读写前后插入内存屏障禁止指令重排。注意volatile 不保证原子性典型例子是 i读、改、写三步volatile 只能保证读到的值是最新但改的过程不是原子的并发下仍然会丢更新。ThreadLocal 是热门追问点。它的设计是每个线程内部有一个 ThreadLocalMapkey 是 ThreadLocal 本身value 是线程持有的变量副本。正因为每个线程各自持有一份才实现了线程间的变量隔离。面试官一定会问ThreadLocal 会内存泄漏吗答案是会的而且非常常见。ThreadLocal 的 key 是弱引用value 是强引用。线程存活期间key 被回收后 value 却无法被访问如果线程池中线程长时间存活value 就永远无法被回收造成泄漏。解法只有一个每次用完 ThreadLocal 必须调用 remove()。特别是线程池场景线程复用上次请求的 value 会被下次请求看到这是业务事故级别的 bug务必要重视。CAS 是并发工具的基础。核心是三个操作数内存地址、预期值、新值。如果内存值与预期值相同就更新为新值否则不更新并重新读取。CAS 的问题也很经典ABA 问题。一个值从 A 变成 B 又变回 ACAS 无法感知这个变化。解决方案是加版本号Java 里用 AtomicStampedReference。ABA 在大多数业务场景下可以接受但如果你在写无锁数据结构就必须考虑。5. JVM 与性能优化线上问题才是真正分水岭5.1 JVM 内存区域与对象分配JVM 内存模型是面试分水岭答得好坏直接决定你能不能进下一轮。按 Java 8 标准内存区域分五大块程序计数器、虚拟机栈、本地方法栈、堆、元空间之前是方法区Java 8 改为元空间。程序计数器是当前线程执行字节码的行号指示器唯一不会 OOM 的区域。虚拟机栈存储栈帧每个方法调用对应一个栈帧包含局部变量表、操作数栈、动态链接、返回地址。栈深度超过限制就抛 StackOverflowError。堆是对象分配的主要区域分新生代和老年代新生代又分 Eden、Survivor From、Survivor To默认比例是 8:1:1。对象优先在 Eden 分配Minor GC 之后存活对象进入 Survivor经历一定年龄后进入老年代。这里可以主动提一句大对象会直接进入老年代避免新生代频繁复制大对象的开销。元空间存类元数据跟永久代的区别是它使用本地内存默认没有上限受操作系统内存限制。所以元空间 OOM 往往是类加载过多比如热部署场景没清理干净。对象分配还有个重点栈上分配与 TLAB。JVM 开启逃逸分析后如果对象不会逃逸出方法可以在栈上分配方法结束自动销毁减少 GC 压力。线程在 Eden 里有自己的 TLAB 缓冲区避免多个线程竞争同一块内存。5.2 垃圾回收算法与常见收集器GC 部分先讲三种基础算法标记-清除、复制、标记-整理。标记-清除将存活对象标记后直接清理未标记的内存碎片化严重复制算法把内存分为两块每次只使用一块回收时把存活对象复制到另一块适合存活率低的场景新生代就是这种思路标记-整理先标记存活对象然后把它们向一端移动适合老年代。收集器一般问 CMS 和 G1。CMS 是 Concurrent Mark Sweep以最短停顿为目标四步初始标记、并发标记、重新标记、并发清除。问题在于会产生内存碎片并发阶段占用 CPU。G1 是 JDK9 默认收集器把堆划分为多个大小相等的 Region维护一个可预测停顿时间模型目标是控制 GC 停顿时间在指定毫秒内。G1 的突出能力是混合回收同时处理新生代和老年代。这里有个小建议回答时加上“JDK8 默认是 Parallel Scavenge Parallel OldJDK9 之后默认 G1”这个知识面试官会认为你确实用过不同版本的 JDK。如果还能说出 G1 的 Remember Set 记录跨 Region 引用那是明显的加分项。5.3 类加载与双亲委派类加载机制问得不少核心是三个阶段加载、连接验证、准备、解析、初始化。加载阶段通过全限定名获取二进制字节流生成 Class 对象。准备阶段为静态变量分配内存并赋默认值初始化阶段才执行赋值和静态块。双亲委派模型是重点类加载请求先给父加载器父加载器无法加载才传给子加载器。这样做的原因主要是安全java.lang.String 这种核心类只能由 Bootstrap ClassLoader 加载不会被用户自定义类覆盖也就避免了核心类被篡改。经典的反问是能不能打破双亲委派可以重写 loadClass 方法跳过父加载器。Tomcat 的 WebAppClassLoader 就打破了双亲委派因为每个 webapp 需要加载自己版本的类。SPI 机制比如 JDBC也通过 ThreadContextClassLoader 逆向加载也算一种破坏。5.4 一次 OOM 排查的完整思路这个问题面试官特别爱用场景题来考。经历过的人答得行云流水没经历过的就只会背参数。我把标准排查路径分享出来。第一步确定类型。看异常栈是堆 OOM、栈溢出、元空间 OOM 还是直接内存 OOM。第二步保留现场。线上立即加上 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump 参数重启前先把 dump 文件保留住。第三步用 MAT 或 jvisualvm 分析 dump 文件查看 Dominator Tree找占用最大的对象。第四步看 GC 日志确认是内存泄漏还是内存分配太快导致的溢出。第五步看代码定位如果是大 List 缓存了全量数据就要改成分批处理如果是泄漏要找到错误持有关键路径。我遇到最典型的例子是 ThreadLocal 配合线程池导致的值泄漏还有本地缓存没有设置过期时间和大小上限最终把堆打爆。这类题目回答时重要的是体现思路清晰而不是一口气背一堆命令。你能把 jmap、jstat、jstack 这几个工具在一分钟内讲清楚各自用途就已经超过大多数候选人了。6. Spring 与 MyBatis框架八股里的重点6.1 Bean 生命周期与三级缓存解决循环依赖Spring 这块问的最多的是 Bean 生命周期。完整执行顺序非常长我建议记忆主线实例化、属性填充、初始化、使用、销毁。细化后要能说出几个关键扩展点BeanNameAware、BeanFactoryAware、ApplicationContextAware 是在属性填充后触发BeanPostProcessor 的 postProcessBeforeInitialization 在 init-method 前执行postProcessAfterInitialization 在 init-method 后执行。AOP 代理就是通过 BeanPostProcessor 的后置处理生成的这句话是连接 Spring 和 AOP 的关键。循环依赖也是高频题。为什么 Spring 能解决 setter 注入的循环依赖却解决不了构造器注入的循环依赖这就要讲到三级缓存。一级缓存 singletonObjects 存成品二级缓存 earlySingletonObjects 存半成品三级缓存 singletonFactories 存对象工厂。创建 A 时A 提前把自己暴露到三级缓存A 在属性填充时发现需要 B就去创建 BB 创建时发现需要 A就从三级缓存拿到 A 的早期引用完成注入之后 A 继续完成创建最终放入一级缓存。构造器注入在 Bean 实例化时就要求依赖对象完整存在此时 A 还没暴露三级缓存所以无解。这个点说清楚Spring 这块基本过关。6.2 事务失效的七种常见场景事务失效是我在面试里必问的一题因为工作中踩坑率极高。我把最常见的情况总结一下面试时能说出五种以上就很有竞争力。第一方法被 private 修饰。Spring 事务基于代理private 方法无法被子类代理事务注解形同虚设。第二同类内部调用。同类中一个方法调用另一个带 Transactional 的方法走的是 this 调用不经过代理对象事务不生效。解法是注入自身代理或者拆到另一个 Service。第三异常被 try-catch 吞掉。事务感知不到异常自然不会回滚。第四抛出的是受检异常。默认只对 RuntimeException 回滚IOException 这类需要手动指定 rollbackFor。第五方法不是 public。虽然 JDK 代理也能拦截非 public 方法但很多配置下不生效规范上要求 public。第六数据库引擎不支持事务比如 MyISAM。第七多线程调用。事务上下文默认不传播到子线程子线程里的操作不在同一个事务里。6.3 MyBatis #{} 与 ${}、一级缓存二级缓存MyBatis 基础题第一问#{} 和 ${} 有什么区别#{} 是预编译占位符会替换成 ?通过 PreparedStatement 传参能防止 SQL 注入${} 是字符串直接拼接有注入风险。所以表名、列名这种不能预编译的用 ${}但要手动校验其他一律用 #{}。缓存机制也是一个考点。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 内相同查询直接命中缓存。坑在于 SqlSession 中执行 update 操作会清空缓存而且 Spring 管理下每次操作 sqlSessionTemplate 可能复用同一个 SqlSession这时候一级缓存可能导致读到脏数据。二级缓存是 namespace 级别多个 SqlSession 共享默认不开启。开启要慎重多表操作时缓存可能产生脏数据我建议项目里默认不开二级缓存。MyBatis 接口没有实现类为什么能直接注入使用这个问题能考验源码理解。MyBatis 扫描 Mapper 接口后用 MapperProxy 生成 JDK 动态代理对象调用接口方法时通过代理去解析对应的 MappedStatement最后执行 SQL。能把这个链路讲清楚框架部分的分就拿到手了。7. Spring Boot 自动配置与 MySQL 索引7.1 Spring Boot 自动配置原理Spring Boot 的自动配置是它最核心的能力也是面试常客。你要能说清楚 SpringBootApplication 是一个组合注解等价于 SpringBootConfiguration、EnableAutoConfiguration、ComponentScan 三个注解的组合。其中 EnableAutoConfiguration 是触发自动配置的开关。自动配置的加载链路是这样的EnableAutoConfiguration 通过 Import 引入 AutoConfigurationImportSelector该类会读取 classpath 下 META-INF/spring.factories 或 AutoConfiguration.imports 文件里的自动配置类列表然后按条件注解过滤生效。条件注解是核心比如 ConditionalOnClass 检查指定类是否存在ConditionalOnProperty 检查配置项是否存在ConditionalOnMissingBean 保证用户自定义 Bean 优先。这里我建议讲一个实际的例子RedisAutoConfiguration 通过 ConditionalOnClass(RedisOperations.class) 判断你有没有引入 Redis 依赖引入才生效同时通过 ConditionalOnMissingBean 保证你自定义 RedisTemplate 时不会被打包配置覆盖。把整个链路串下来比单纯背概念有说服力得多。7.2 MySQL 为什么用 B 树做索引MySQL 索引题的重点是 B 树。面试问题通常是为什么 InnoDB 索引选择 B 树而不是 B 树、红黑树或哈希回答这道题从磁盘 IO 的角度切入最深刻。数据库索引需要持久化到磁盘磁盘读取是 IO 操作耗时远大于内存访问。B 树的高度一般只有 2 到 4 层千万级数据量下只需要 3 次左右磁盘 IO 就能定位数据。对比一下红黑树高度在 log2n 级别数据量一大磁盘 IO 次数急剧增多这在数据库场景下不可接受。B 树的非叶子节点只存储键值不存储数据所以单个节点能存更多索引树更矮IO 更少。B 树相比 B 树的另一个关键优势是叶子节点之间有双向链表适合范围查询和排序MySQL 的区间扫描就是顺着链表遍历。Hash 索引查询是 O(1)但不支持范围查询也无法排序所以 InnoDB 默认用 B 树。还要分清聚簇索引和二级索引。InnoDB 表必有聚簇索引默认是主键叶子节点存储整行数据二级索引叶子节点存储主键值。通过二级索引查询需要回表也就是先查二级索引拿到主键再回聚簇索引取整行数据。覆盖索引能避免回表是 SQL 优化的常用手段。7.3 索引失效与 SQL 调优的实战经验索引失效是面试里最喜欢出的场景题。常见的失效情况要背到张口就来并且理解背后的原因。第一对索引列使用函数或运算比如 WHERE YEAR(create_time) 2024索引失效应该改成 create_time 范围条件。第二隐式类型转换手机号字段是 varchar查询条件写成数字导致全表扫描。第三左模糊查询 LIKE %abc前缀不确定用不上 B 树有序性索引失效右模糊 LIKE abc% 可以走索引。第四OR 连接非索引字段导致整体无法走索引要改成 UNION。第五联合索引不满足最左前缀法则。SQL 调优的第一步永远是 EXPLAIN。我会教人先看 type 字段它的好坏顺序是 system const eq_ref ref range index ALL。出现 ALL 就要重点排查。再看 possible_keys 和 key确认优化器是否选择了正确的索引。如果命中了索引但 rows 估算行数依然很大考虑是不是用了错误的索引。实际调优中最有效的一个建议是别迷信索引索引不是越多越好。每个索引都是写放大插入和更新都要维护索引。我见过一张表建了十几个索引写性能惨不忍睹。优先保证高频查询命中最少索引低频率的大范围统计可以接受扫描或单独走数据仓库。8. Redis 缓存与分布式锁8.1 缓存穿透、击穿、雪崩附解决方案Redis 缓存三板斧是分布式岗位的必考题。我直接说结论和方案。缓存穿透是指查询一个不存在的数据缓存和数据库都没有导致请求直接打到数据库。解决方案两个一是缓存空值把不存在的 key 也缓存起来设置较短过期时间二是布隆过滤器请求先经过布隆过滤器判断一定不存在的 key 直接拦截。布隆过滤器的误判率要提前评估它只能保证不存在的一定不在存在的不一定在。缓存击穿是指某个热点 key 过期瞬间大量并发请求同时打进数据库。解法是互斥锁缓存过期后只让一个请求去查数据库重建缓存其他请求等待另一种是逻辑过期value 里保存过期时间后台异步刷新查询时不等待。实际项目我用逻辑过期更顺手因为不会让请求链路长时间阻塞。缓存雪崩是指大量 key 同一时间失效或 Redis 实例宕机请求全部打到数据库。key 过期错开用固定随机值或者业务标识取模做过期时间偏移Redis 高可用用主从加哨兵或 Cluster宕机自动切换。还要在应用侧做兜底接口层限流降级。8.2 分布式锁的实现与选型很多系统走到微服务阶段后分布式锁需求就来了。面试问法通常是Redis 分布式锁怎么实现有什么问题会不会用 Zookeeper为什么Redis 锁最基础的实现是 SET key value NX PX 30000NX 保证原子地设置不存在的 keyPX 设置过期时间防止锁永不释放。value 要存一个唯一标识释放时先比较再删除这一步必须用 Lua 脚本保证原子性否则可能出现误删别人锁的问题if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endRedis 锁的经典问题是锁过期时间不好设置。业务执行时间超过过期时间锁自动释放别的线程拿到锁此时两个线程同时执行业务锁就失效了。业界方案是 Redisson 的看门狗机制后台线程自动续期默认每 10 秒续一次。另一个问题是主从切换可能丢锁极端情况下主节点挂了锁还没同步到从节点其他节点会拿到同一把锁。更严格的方案是 RedLock但要正确实现 RedLock 条件很苛刻我在实际项目里倾向于用 Zookeeper 的临时顺序节点或者直接用 Redisson 并接受极低概率的风险。Zookeeper 锁的原理是临时顺序节点加 Watch 监听。创建临时节点后客户端只需监听前一个节点是否被删除删除就说明自己获得了锁。Zookeeper 的临时节点会在会话断开时自动删除不会出现锁永远不释放的问题比 Redis 过期时间更可控。劣势是性能不如 Redis适合锁并发要求不高但对一致性要求高的场景。这道题我的回答建议是先说 Redis 的轻量和高性能再说潜在问题最后根据业务场景选择而不是无脑推荐某一方案。9. 面试实战避坑把准备转化成 Offer9.1 被问到不会的题怎么办面试中遇到不会的题太正常了重点是怎么处理。最忌硬编明明不懂还要强行解释面试官两句话就能看出来反而印象分大减。我的经验是先确认问题复述一遍“您问的是不是 XXX 在 XX 场景下的选择”一方面给自己争取几秒思考时间另一方面确认没有理解偏。如果确实不会坦诚说这个方向我没有深入过然后用已有知识做一个合理推演。举个例子问到没接触过的分布式事务 Seata你可以说我没在线上用过 Seata但分布式事务大致绕不开两阶段提交的思路先预提交再确认提交据我了解 Seata 把分支事务的协作交给 TC业务侧实现 TCC 或者 AT 模式。这种回答展示了知识迁移能力比直接说不知道强太多。另外面试官问到某个点如果你懂就顺着往下多说一点并抛出延展话题比如主动说“这块跟 XX 其实原理类似”这能引导面试官往你熟悉的领域继续问。9.2 项目经验怎么讲才不虚项目介绍是几乎所有面试的必由之路但绝大多数候选人讲得很空虚。要么从头到尾讲需求功能要么只讲用了什么技术栈面试官根本提取不到有效信息。正确的讲法是用问题驱动项目背景、核心难点、你的方案、最终效果、踩了什么坑。背景不要说“公司要做个商城”要明确业务背景和约束条件。难点要说带冲突的事比如“千万级库存更新并发扣减怎么保证不超卖”就比“我负责订单模块”有信息量。方案要说技术选型理由并对比过其他方案。效果要量化比如“接口耗时从 800ms 降到 150ms”。坑一定要真实哪怕很小比如“上线前才发现缓存穿透把数据库打挂了紧急加布隆过滤器”这种细节反而让人信服。9.3 写到最后几句实在话整理这份题库的过程里我反复在想一个问题面试到底在考什么。其实面到很深的层级面试官自己也知道你不会什么都用过他们更在意的是你面对未知时有没有思路、面对压力时能不能稳住、面对过去踩过的坑有没有总结。所以刷这些题的时候不要只求“记住”试着用四段式答题法自己讲一遍录下来听听哪里不顺、哪里太虚。多讲几遍你自然能找到那种“我确实干过这活”的底气。我还有一个小技巧准备面试的时候把每道题都当成一个故事来讲。String 不可变不是什么冰冷的概念而是字符串常量池能缓存、哈希能复用的底层前提HashMap 树化不是死记阈值 8而是泊松分布下的概率防御。当你能从一个知识点自然关联到下一个知识点并且带上真实的工程案例面试官想不给你高分都难。这份新鲜热乎的大全二三不会再更新了但里面的知识体系值得你反复吃透。
返回列表