公司动态
Java后端面试高频考点全梳理:从JVM到Redis一网打尽
8月的招聘市场进入下半年冲刺阶段很多同学开始集中刷Java后端面试题。网上的资料确实多但碎片化严重今天看一篇集合源码明天看一篇JVM垃圾回收后天又去背Spring事务最后真正面试时依然答不到点上。这篇文章我按后端面试的高频考察顺序把Java基础、JVM、并发、Spring、MySQL、Redis、消息队列、手撕算法等核心考点做一次系统梳理。每一道题都会给出“面试官考察点”和“推荐答题思路”并配上可运行的代码示例。全文覆盖的题目接近后端面试中出现频率最高的那批适合正在准备秋招、社招或者想系统复习Java后端知识点的开发者。需要先说明一点面试没有“必过秘籍”也没有哪个题库能保证100%通过。真正决定面试结果的是你对原理的理解深度、项目表达的清晰度以及临场沟通的稳定性。这份梳理能帮你减少无效复习把时间花在最高频、最核心的知识点上。1. 8月Java后端面试到底在考什么后端岗位的面试题虽然每年都有新变化但核心考察结构一直比较稳定。无论是字节、阿里、腾讯还是中小型互联网公司面试官通常会围绕下面几个纬度提问考察纬度常见问题方向占比预估Java基础String、集合、异常、泛型、反射15% - 20%JVM内存区域、垃圾回收、类加载、OOM10% - 15%并发编程synchronized、volatile、线程池、锁15% - 20%Spring家族IOC、AOP、事务、Spring Boot自动配置15% - 20%数据库MySQL索引、事务隔离、锁、SQL优化15% - 20%中间件Redis缓存、消息队列、分布式锁10% - 15%手撕代码排序、链表、动态规划、在线编程10% - 15%项目与场景项目架构、难点、线上问题排查贯穿全程从这个结构能看到后端面试不只是背八股它更看重“基础原理 工程实践 场景设计”的综合能力。所以接下来的梳理我不会只给一句话结论而是把最需要展开理解的部分写清楚。2. Java基础高频面试题2.1 String、StringBuffer、StringBuilder 的区别面试官问这道题通常还会继续追问“String为什么是不可变的”“字符串拼接用什么更好”。答题时不能只背结论要说明底层原因。先说结论String是不可变字符串每次拼接都会生成新对象。StringBuffer是可变字符串线程安全方法用synchronized修饰。StringBuilder是可变字符串线程不安全但性能更高。源码层面来看String类内部用private final char value[]保存字符并且类本身被final修饰不允许子类覆盖行为。所以一旦创建字符串内容不可改变。这里有一个高频追问为什么 String 要设计成不可变好的回答方向字符串常量池复用。不可变才能安全地缓存字符串引用避免相同字符串在内存中重复存储。安全性。String 被大量用作方法参数、文件路径、网络连接地址如果可变容易带来安全风险。线程安全。不可变对象天然线程安全无需额外加锁。哈希缓存。String 的 hash 值可以安全缓存因为内容不会变。再看一道常见代码题String str1 hello; String str2 hello; String str3 new String(hello); System.out.println(str1 str2); // true System.out.println(str1 str3); // false System.out.println(str1.equals(str3)); // true比较的是引用地址equals比较的是内容。str1和str2都指向字符串常量池中的同一个对象str3是堆上新建的对象。循环拼接字符串时推荐使用StringBuilderStringBuilder sb new StringBuilder(); for (int i 0; i 1000; i) { sb.append(i); } String result sb.toString();2.2 equals 与 hashCode 的关系这是 Java 基础中几乎必问的题目。面试官考察的是你知不知道HashSet、HashMap去重和查找的底层逻辑。核心知识点equals()用于判断两个对象是否“相等”。hashCode()返回对象的哈希值。如果两个对象equals相等则hashCode必须相等。如果两个对象hashCode相等equals不一定相等这种情况叫哈希冲突。为什么必须满足第一条因为HashMap的put和get流程是先根据hashCode定位桶再在桶内用equals比较链表或红黑树中的节点。如果重写了equals却没有重写hashCode有两个对象在业务上相等但哈希值不同就会出现在不同桶里导致HashMap中出现重复“相等”的对象。标准重写示例public class User { private String name; private Integer age; public User(String name, Integer age) { this.name name; this.age age; } Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return Objects.equals(name, user.name) Objects.equals(age, user.age); } Override public int hashCode() { return Objects.hash(name, age); } }答题时还可以补充hashCode的实现一般用关键字段组合计算Objects.hash()底层会处理 null 值避免空指针。2.3 ArrayList、LinkedList、HashMap 底层原理这些集合类题目属于“代码背得再多也不嫌多”的考点。面试官非常喜欢从底层结构出发不断追问。ArrayList 与 LinkedList 的区别对比项ArrayListLinkedList底层结构动态数组双向链表随机访问O(1)O(n)插入删除尾部O(1)中间O(n)头尾O(1)中间O(n)内存占用连续空间有扩容开销每个节点额外存前后指针适用场景查询多、按索引访问频繁头尾增删高频追问ArrayList 的扩容机制是什么ArrayList 默认容量是 10当元素数量超过当前容量时会扩容为原来的 1.5 倍也就是oldCapacity (oldCapacity 1)。扩容过程需要Arrays.copyOf把原数组拷贝到新数组因此频繁扩容会带来性能损耗。创建时可以预估大小例如new ArrayList(100)。HashMap 底层原理HashMap 是面试中的重头戏推荐按“JDK 1.7 → JDK 1.8 演变”的方式回答JDK 1.7数组 链表头插法扩容时可能出现死循环。JDK 1.8数组 链表 红黑树尾插法链表长度超过 8 且数组长度达到 64 时转红黑树。put流程大致是计算key的hashCode()再通过扰动函数混合高位信息。用(n - 1) hash计算桶下标。如果桶为空直接插入新节点。如果桶不为空遍历链表或红黑树有相同 key覆盖 value。没有相同 key插入链表尾部或红黑树。为什么要用(n - 1) hash因为 HashMap 的容量是 2 的幂次方n - 1的二进制低位全是 1这样可以让散列值均匀分布同时位运算比取模更快。2.4 异常体系与 finallyJava 异常题目不会单独出太多但经常会结合代码题或者项目经验问。Java 的异常体系以Throwable为根分两个分支Error程序无法处理的严重问题比如OutOfMemoryError、StackOverflowError。Exception程序可以处理的异常又分受检异常和非受检异常。受检异常编译期强制要求处理比如IOException、SQLException。非受检异常也叫运行时异常比如NullPointerException、ArrayIndexOutOfBoundsException编译期不强制捕获。一个容易被追问的题目finally 中的 return 会覆盖 try 中的 return 吗会。如果 finally 里也有 return那么 finally 的返回值会覆盖 try 或 catch 中的返回值。public static int test() { try { return 1; } finally { return 2; } } System.out.println(test()); // 输出 2实际开发中尽量不要在 finally 里写 return容易掩盖异常。如果 finally 中需要释放资源更推荐使用 try-with-resources。try (BufferedReader reader new BufferedReader(new FileReader(test.txt))) { String line reader.readLine(); } catch (IOException e) { log.error(读取文件失败, e); }2.5 泛型与类型擦除泛型题目通常会问泛型是编译期还是运行期的机制Java 的泛型是编译期机制虚拟机运行时并不认识泛型。ListString和ListInteger在字节码层面都是List这叫类型擦除。例如ListString list new ArrayList(); list.add(hello); // 通过反射绕过泛型检查放入 Integer Method addMethod list.getClass().getMethod(add, Object.class); addMethod.invoke(list, 123); System.out.println(list); // [hello, 123]从这里可以看到编译期检查只能约束正常代码路径反射可以绕过。理解类型擦除也能帮助理解为什么泛型类不能创建泛型数组、不能直接T.class获取类型。3. JVM 高频面试题3.1 JVM 内存区域划分JVM 是后端面试里最让新手头疼的部分但也是区分度最高的部分。建议先牢牢掌握内存区域划分。JVM 运行时数据区可以分为线程私有和线程共享两大部分内存区域线程共享作用常见异常程序计数器私有记录当前线程执行字节码的行号无Java 虚拟机栈私有方法调用和局部变量存储StackOverflowError本地方法栈私有native 方法调用StackOverflowError堆共享对象实例和数组OutOfMemoryError方法区 / 元空间共享类信息、常量、静态变量OutOfMemoryError需要注意的是JDK 1.8 之后永久代被移除方法区改为元空间Metaspace。元空间不再使用虚拟机内存而是使用本地内存默认上限跟操作系统内存有关。3.2 垃圾回收算法与垃圾收集器面试官问垃圾回收核心是想知道你有没有理解“标记—清除—整理—分代收集”这一套思路。三块基础算法标记-清除先标记可回收对象再统一清除。缺点是会产生内存碎片。复制算法将内存分成两块每次只使用一块回收时把存活对象复制到另一块。优点是不会有碎片缺点是浪费空间。标记-整理标记存活对象后把存活对象向一端移动然后清理边界以外内存。适合老年代。实际 JVM 使用的是分代收集理论新生代对象朝生夕死用复制算法。分为 Eden、S0、S1默认比例 8:1:1。老年代对象存活率高用标记-整理或标记-清除。如果继续追问垃圾收集器可以提到 Serial、Parallel、CMS、G1。G1 是目前 JDK 默认垃圾收集器之一它的特点是把堆划分为多个 Region可以做到可预测的停顿时间。这里不要求背出所有收集器的参数但至少要能说出 CMS 和 G1 的核心区别CMS 主要面向老年代目标是缩短停顿时间基于标记-清除会产生碎片。G1 面向整个堆基于 Region 划分和复制算法能更好地控制最大停顿时间。3.3 类加载过程与双亲委派模型类加载过程可以按顺序记忆为五个阶段加载、验证、准备、解析、初始化。高频追问是双亲委派模型当一个类加载器收到类加载请求时它不会自己先去加载而是把请求委派给父类加载器。每一层都是如此所以最终会传到最顶层的启动类加载器。只有在父类加载器无法加载时子类加载器才会尝试自己加载。这样做的原因很关键保证 Java 核心类库的安全防止用户自定义的java.lang.String覆盖 JDK 自带的 String。经典问题能否自己写一个 java.lang.String一般来说不行。根据双亲委派模型加载java.lang.String时会优先交给启动类加载器加载到的是 JDK 自带的 String自定义类不会被加载。即使尝试覆盖也会有安全性限制。3.4 OOM 常见场景与分析OOM 是线上 Java 服务最常见的问题之一面试官喜欢结合线上场景让你排查。常见 OOM 类型异常信息可能原因Java heap space对象过多堆内存不足Metaspace类加载过多动态生成类太多Unable to create new native thread线程数达到系统上限GC overhead limit exceededGC 回收效率太低频繁 Full GC排查路线建议这样回答查看监控确认 OOM 发生时间点留意 GC 日志。使用jmap -dump:formatb,fileheap.hprof pid导出堆快照。用 MAT 或 VisualVM 分析大对象和引用链。结合代码定位是内存泄漏还是内存溢出。如果是内存泄漏重点检查连接资源是否关闭、静态集合是否无限增长、ThreadLocal 是否清理。如果是内存溢出考虑调整堆参数或优化代码结构。4. 并发编程高频面试题4.1 synchronized 与 volatile 的区别并发编程是 Java 后端面试的难点同时也是日常工作排查线上问题的基础。先说结论volatile保证可见性和有序性但不保证原子性。synchronized保证可见性、有序性和原子性。为什么 volatile 不保证原子性看下面代码public class VolatileTest { private static volatile int count 0; public static void main(String[] args) throws InterruptedException { Thread[] threads new Thread[10]; for (int i 0; i 10; i) { threads[i] new Thread(() - { for (int j 0; j 1000; j) { count; } }); threads[i].start(); } for (Thread t : threads) { t.join(); } System.out.println(count); } }count不是原子操作它在底层分为读取、修改、写回三步。即使加了 volatile多个线程依然可能同时读取到同一个值然后各自写回导致计数丢失。所以最终结果大概率小于 10000。解决方式有几种使用synchronized修饰方法或代码块。使用AtomicInteger这种 CAS 原子类。4.2 锁升级过程Java 6 之后synchronized 引入了锁升级机制。面试官问这个是想确认你不只停留在“synchronized 是重量级锁”的旧认知上。锁升级路径无锁 - 偏向锁 - 轻量级锁 - 重量级锁偏向锁同一线程多次获取锁时消除重入开销。轻量级锁存在锁竞争但竞争不激烈时通过 CAS 自旋获取锁。重量级锁竞争激烈时升级为操作系统层面的互斥锁阻塞等待线程。这个机制的设计思路是大部分场景下锁竞争并不激烈没必要一上来就用最重量级的实现。4.3 线程池参数与拒绝策略线程池是 Java 并发中最贴近实际开发的内容。面试官常给一个场景让你配置线程池参数并说明为什么。ThreadPoolExecutor 的核心参数有七个new ThreadPoolExecutor( corePoolSize, // 核心线程数 maximumPoolSize, // 最大线程数 keepAliveTime, // 空闲线程存活时间 TimeUnit.SECONDS, // 时间单位 new LinkedBlockingQueue(100), // 任务队列 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 );任务执行流程当前线程数小于核心线程数时创建新线程执行任务。线程数达到核心线程数后任务放入队列。队列满后继续创建线程直到达到最大线程数。线程数达到最大值且队列已满触发拒绝策略。四种拒绝策略AbortPolicy直接抛出 RejectedExecutionException。CallerRunsPolicy由调用者线程执行任务。DiscardPolicy直接丢弃新任务。DiscardOldestPolicy丢弃队列中最老的任务然后重试提交。这里要强调一点阿里 Java 开发手册不建议使用 Executors 创建线程池。因为newFixedThreadPool和newSingleThreadExecutor允许的请求队列长度为Integer.MAX_VALUE可能导致内存堆积newCachedThreadPool允许创建线程数量为Integer.MAX_VALUE可能导致创建过多线程。所以推荐手动通过 ThreadPoolExecutor 创建。4.4 CAS 与 AQSCASCompare And Swap是一种无锁算法。它包含三个参数内存地址 V、旧值 A、新值 B。只有当 V 的值等于 A 时才把 V 更新为 B。AtomicInteger 就是基于 CAS 实现的。AtomicInteger count new AtomicInteger(0); count.incrementAndGet();CAS 的缺点也要能说出来ABA 问题变量被其他线程从 A 改成 B 又改回 ACAS 无法感知。解决方式是用AtomicStampedReference加版本号。自旋开销大并发高时大量 CAS 失败CPU 空转。只能保证一个共享变量的原子操作。AQSAbstractQueuedSynchronizer是很多 JUC 同步器的基础ReentrantLock、Semaphore、CountDownLatch 底层都是 AQS。AQS 核心是一个volatile int state和一个CLH 双向队列。获取资源失败时线程会被封装成节点放入队列等待释放资源时唤醒队列中的后继节点。4.5 手写双重检查锁单例这是一道非常经典的手写题背后涉及的考点包括 volatile、锁、线程安全、类加载。public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }这里为什么要加volatile因为instance new Singleton()在字节码层面不是原子操作它会经历三步在堆上分配内存空间。调用构造方法初始化对象。将引用指向分配的内存。在 JIT 或 CPU 指令重排序的影响下可能先执行第 3 步再执行第 2 步。此时另一个线程进入getInstance()发现instance ! null直接返回一个还没初始化完成的对象。加上volatile可以禁止重排序保证可见性。5. Spring 与 Spring Boot 高频面试题5.1 IOC 与 AOP 是什么Spring 的 IOC控制反转和 AOP面向切面编程是整个框架的基石。IOC 的意思是对象创建的主动权从程序员手里反转给 Spring 容器。程序员不再通过new创建对象而是声明依赖由容器负责实例化和注入。AOP 是把日志、事务、鉴权等通用逻辑从业务代码中抽离出来通过动态代理织入目标方法。Spring AOP 底层有 JDK 动态代理和 CGLIB 两种实现目标类实现了接口默认使用 JDK 动态代理。目标类没有实现接口使用 CGLIB 代理。一个高频追问是Spring 是如何解析 Autowired 并完成注入的简化回答AutowiredAnnotationBeanPostProcessor在 Bean 初始化阶段扫描Autowired标注的字段或方法通过类型查找候选 Bean再按名称和限定符筛选最终用反射完成赋值。5.2 Bean 生命周期Spring Bean 生命周期是全家桶面试中几乎绕不开的题目。推荐按阶段记忆实例化 - 属性填充 - Aware 方法回调 - BeanPostProcessor 前置处理 - 初始化方法 - BeanPostProcessor 后置处理 - 使用 - 销毁更完整一点容器扫描并创建 Bean 实例。填充属性Autowired 注入。调用 BeanNameAware、BeanFactoryAware 等回调。调用 BeanPostProcessor 的 postProcessBeforeInitialization。调用 PostConstruct 或 InitializingBean.afterPropertiesSet 或 init-method。调用 BeanPostProcessor 的 postProcessAfterInitialization。Bean 就绪放进容器。容器关闭时调用 PreDestroy 或 DisposableBean.destroy。面试官如果追问Spring 在哪里支持循环依赖和 AOP 代理关键点就在 BeanPostProcessor 上。5.3 循环依赖与三级缓存循环依赖指的是 A 依赖 BB 又依赖 A。Spring 默认支持单例 Bean 的 Setter 循环依赖但不支持构造器循环依赖。Spring 解决循环依赖依赖三级缓存缓存名称存放内容singletonObjects成品单例 BeanearlySingletonObjects提前暴露的早期 Bean能拿到但未完成全部初始化singletonFactories单例对象工厂用于生成提前引用当 A 创建时发现需要注入 B就去创建 BB 创建时发现需要注入 A就从三级缓存中找到 A 的工厂提前拿到 A 的引用。这样 B 能完成创建A 再继续走完自己生命周期。为什么需要第三级缓存因为在对象实例化后、初始化前可能需要生成 AOP 代理对象。第三级缓存的作用就是延迟创建代理保证普通场景下先从容器里拿原始对象只有发生循环依赖时才提前生成代理。5.4 Spring 事务传播行为与失效场景Spring 事务传播行为用一句话概括多个事务方法之间调用时事务如何传播。常用传播行为传播行为说明REQUIRED有事务则加入没有则新建默认REQUIRES_NEW无论有无事务总是新建事务NESTED嵌套事务内层回滚不影响外层SUPPORTS有事务则加入没有则以非事务方式执行接着高频追问Spring 事务为什么有时会失效常见失效场景方法用 private 修饰。Spring AOP 基于代理private 方法无法被代理拦截。方法内部自调用。同类中的this.method()调用走的是 this 对象不是代理对象事务切面不生效。异常被捕获。事务方法中 catch 了异常却没抛出Spring 感知不到异常事务不会回滚。抛出检查异常。Spring 默认只对 RuntimeException 回滚checked exception 需要配置 rollbackFor。数据库引擎不支持事务比如 MyISAM但这个说法更多是历史遗留现在主流 MySQL 使用 InnoDB。一个比较完整的正确写法Service public class UserService { Transactional(rollbackFor Exception.class) public void updateUser(User user) { userDao.update(user); orderService.createOrder(user.getId()); } }显式指定rollbackFor Exception.class能避免检查异常不回滚的问题。5.5 Spring Boot 自动配置原理Spring Boot 之所以能“开箱即用”核心在于自动配置。SpringBootApplication是一个组合注解包含SpringBootConfiguration声明配置类。EnableAutoConfiguration开启自动配置。ComponentScan扫描当前包及其子包。EnableAutoConfiguration通过Import(AutoConfigurationImportSelector.class)加载META-INF/spring.factories或AutoConfiguration.imports中的自动配置类。每个自动配置类通常配合ConditionalOnClass、ConditionalOnMissingBean等条件注解只有当满足条件时才生效。比如 Redis 自动配置只有在 classpath 中存在 RedisTemplate 相关类时才加载并且如果用户自己定义了RedisTemplateBean自动配置的默认 RedisTemplate 会被覆盖。答题时能把这套“条件装配 自动导入”的机制讲清楚面试官就会知道你不仅会用 Spring Boot还知道它为什么能自动工作。6. MySQL 数据库高频面试题6.1 索引数据结构与失效场景后端开发每天都要跟数据库打交道MySQL 索引几乎是必问。MySQL InnoDB 的索引底层是 B 树。选 B 树的几个原因B 树非叶子节点只存索引不存数据单页能存更多索引树高更矮减少磁盘 IO。叶子节点形成有序链表适合范围查询。所有查询都必须走到叶子节点查询时间稳定。常见索引失效场景对索引列使用函数例如WHERE YEAR(create_time) 2024。隐式类型转换例如手机号字段是 varchar查询时用数字。最左前缀原则不满足联合索引跳过第一个字段。使用LIKE %xxx通配符在开头时索引失效。使用OR连接条件时如果其中一个字段没有索引可能导致整条语句放弃索引。一道高频 SQL 题联合索引 (a, b, c)哪些查询可以走索引SELECT * FROM t WHERE a 1 AND b 2 AND c 3; -- 走索引 SELECT * FROM t WHERE a 1 AND c 3; -- 部分走索引b 中断 SELECT * FROM t WHERE b 2 AND c 3; -- 无法走索引不符合最左前缀6.2 事务隔离级别与 MVCC事务的四大特性 ACID 必须掌握原子性、一致性、隔离性、持久性。MySQL 的四个隔离级别隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交避免可能可能可重复读避免避免InnoDB 中基本避免串行化避免避免避免InnoDB 默认隔离级别是可重复读。它通过 MVCC 解决普通读的幻读问题通过间隙锁解决当前读的幻读问题。MVCC 的核心是隐藏字段和 undo log。每行记录有DB_TRX_ID最近修改该行的事务 ID。DB_ROLL_PTR指向该行之前版本的 undo log。读视图 ReadView 用来判断哪些版本对当前事务可见。简单理解MVCC 让不加锁的普通读可以读到某个快照版本避免读写互相阻塞。6.3 InnoDB 锁机制InnoDB 锁通常分共享锁和排他锁共享锁S允许多个事务同时读同一行。排他锁X允许事务更新或删除其他事务不能加任何锁。从锁粒度来看InnoDB 支持表锁和行锁。行锁又分为记录锁锁住具体一行。间隙锁锁住记录之间的间隙防止插入。临键锁记录锁 间隙锁范围查询时防止幻读。例如执行SELECT * FROM user WHERE id BETWEEN 10 AND 20 FOR UPDATE;如果 id 15 这条记录不存在InnoDB 可能会在 (10, 20) 之间加间隙锁阻止其他事务插入 id 12 的新记录。6.4 SQL 优化思路面试官问 SQL 优化期待的答案不是“加索引”三个字而是一套排查思路。推荐按这个顺序回答使用 EXPLAIN 查看执行计划关注 type、key、rows、Extra 字段。检查是否索引失效。避免SELECT *只查询必要字段。分页深时使用延迟关联。例如深分页优化前SELECT * FROM order WHERE status 0 ORDER BY id LIMIT 100000, 20;深分页优化后SELECT o.* FROM order o INNER JOIN (SELECT id FROM order WHERE status 0 ORDER BY id LIMIT 100000, 20) t ON o.id t.id;先通过覆盖索引快速定位需要的 id再回表查询完整数据减少回表次数。7. Redis 高频面试题7.1 Redis 数据结构与适用场景Redis 是后端中间件里考察频率最高的一个。先要把五种基础数据结构讲清楚数据结构底层实现常见场景StringSDS缓存、计数器、分布式锁Hash哈希表 zipList缓存对象List双向链表 zipList消息队列、最新列表Set哈希表 intset去重、交集并集ZSet跳跃表 哈希表排行榜、延迟队列高频追问ZSet 为什么用跳跃表而不是红黑树回答方向跳跃表实现简单更易理解ZSet 支持范围查询跳跃表在范围查找时效率较高并发修改情况下跳跃表的调整成本相对可控。7.2 缓存穿透、缓存击穿、缓存雪崩这是 Redis 面试题中“三板斧”必须会答。缓存穿透查询一个不存在的数据缓存和数据库都没有请求直接打到数据库。解决思路缓存空值并设置较短过期时间。使用布隆过滤器先在过滤器拦截不存在的 key。缓存击穿某个热点 key 突然过期大量并发请求同时打到数据库。解决思路热点 key 不设置过期时间。加互斥锁让只有一个请求去加载数据库其他线程等待。缓存雪崩大量 key 在同一时间过期或者 Redis 集群整体宕机导致数据库压力骤增。解决思路过期时间加随机值。使用多级缓存比如结合本地缓存。做好 Redis 高可用比如哨兵或集群。限流和降级兜底。7.3 Redis 持久化机制Redis 持久化有两种RDB 和 AOF。RDB 是快照持久化按一定条件把内存数据保存为二进制文件。优点是恢复快、性能影响小缺点是可能丢失最后一次快照之后的数据。AOF 是追加日志记录每次写命令。优点是数据更安全缺点是文件体积较大恢复速度相对较慢。AOF 重写机制可以压缩日志文件。实际生产环境通常是两种持久化方式结合使用RDB 做数据恢复AOF 保证数据尽量不丢失。7.4 Redis 分布式锁分布式锁在微服务项目中出现频率极高。Redis 实现分布式锁的核心命令是SET NX EXboolean locked redisTemplate.opsForValue() .setIfAbsent(lock:order:1001, token, 30, TimeUnit.SECONDS); if (locked) { try { // 执行业务逻辑 } finally { // 释放锁需要校验 value 是否仍属于当前线程 String currentValue (String) redisTemplate.opsForValue().get(lock:order:1001); if (token.equals(currentValue)) { redisTemplate.delete(lock:order:1001); } } }释放锁为什么不能直接delete因为锁可能已经自动过期被其他线程拿到直接删除会误删别人的锁。所以要先判断 value 是否一致再用 Lua 脚本保证“判断 删除”的原子性。更严格的场景中使用 Redisson它提供的锁支持看门狗机制可以自动续期避免业务逻辑没执行完锁就过期了。8. 消息队列与分布式高频面试题8.1 Kafka 为什么快Kafka 作为主流消息队列经常被问到性能问题。Kafka 高性能的原因可以概括为顺序写磁盘以追加方式写入日志利用磁盘顺序读写的性能优势。页缓存大量读写操作命中操作系统页缓存减少磁盘 IO。零拷贝消费者读取消息时数据从磁盘到内核缓冲区后直接通过网卡发送减少复制次数。分区并行不同分区可以并行读写提高吞吐量。批量发送和批量拉取减少网络往返和频繁 IO。8.2 如何保证消息不丢失、不重复消息队列的问题本质上可以拆成三段来思考生产端、Broker、消费端。生产端不丢失Kafka 生产者设置acks all表示所有副本都确认后才返回成功。同时配置合理重试次数。Broker 不丢失Topic 的副本因子设置大于 1比如 3。确保min.insync.replicas不小于 2。消费端不丢失关闭自动提交位移手动在业务处理成功后再提交 offset。这样可以避免业务没处理完就提交 offset导致消息丢失。不重复消费消息队列无法完全避免重复只能消费端幂等处理。常见幂等方案唯一主键或唯一索引约束。在数据库表中记录唯一业务流水号。使用 Redis SETNX 做去重标记。8.3 分布式事务常见方案分布式事务是后端高级职位的常考题主要方案有方案核心思想适用场景2PC 两阶段提交准备 提交强一致性场景但性能较差TCCTry、Confirm、Cancel对一致性要求高的业务本地消息表本地事务 消息表最终一致性简单可靠消息事务半消息 回查RocketMQ 支持Seata AT 模式全局事务框架自动补偿微服务项目落地常用如果项目经验里没有涉及分布式事务不要硬编。可以诚实说“我们没有遇到强一致需求主要用最终一致性方案”然后展开思路。8.4 Cookie、Session 与 Token后端开发面向前后端分离项目时登录认证是必问内容。Session 粘性运维困难所以现在更多使用 Token 方案。JWT 的典型结构是Header.Payload.SignatureJWT 可以携带用户信息服务端无状态校验适合分布式环境。但 JWT 也有缺点无法主动吊销过期时间不好控制payload 可以解码但不应放敏感信息。所以登录鉴权话题中可以补充说明“我更推荐把 access token 和 refresh token 分开使用必要时配合黑名单”。9. 手撕代码高频题手撕代码环节重点考察编码基本功这里整理了出现频率最高的四道题代码可以独立运行。9.1 两数之和public int[] twoSum(int[] nums, int target) { MapInteger, Integer map new HashMap(); for (int i 0; i nums.length; i) { int diff target - nums[i]; if (map.containsKey(diff)) { return new int[]{map.get(diff), i}; } map.put(nums[i], i); } return new int[0]; }注意点先判断再 put。如果把所有元素先放入 map相同元素的值会被后一个覆盖可能造成结果错误。9.2 反转链表public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next prev; prev curr; curr next; } return prev; }这道题能考察链表基础建议先口头讲清楚三步保存后继节点、修改当前节点指针、移动 prev 和 curr。9.3 快速排序public void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } arr[i] arr[j]; while (i j arr[i] pivot) { i; } arr[j] arr[i]; } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }平均时间复杂度 O(n log n)最坏情况 O(n²)发生在每次选中的基准值都是最大或最小元素时。9.4 冒泡排序public void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { boolean swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }冒泡排序本身不复杂但能写出“提前退出”优化的人说明有代码优化意识。10. 面试表达与避坑指南10.1 如何组织一道面试题的答案很多同学知识点都懂但回答时逻辑混乱面试官听得费劲。推荐采用“结论先行 展开细节 场景收尾”的结构。例如问到线程池先说结论线程池是管理复用线程的工具核心参数有七个。展开细节分别解释 corePoolSize、maximumPoolSize、队列、拒绝策略。场景收尾如果是我来做我会结合任务类型设置参数并手动创建线程池。这种答题方式既能让面试官快速抓住重点也能体现工程思维。10.2 常见卡壳点排查面试中常见问题可能原因解决思路只背了结论答不出原理复习流于表面多问自己“底层怎么实现”“为什么这样设计”源码题答得很浅没看过源码重点看 HashMap、ArrayList、ThreadPoolExecutor 关键流程项目回答没有亮点缺少复盘提前准备一个线上问题排查案例手撕代码编译不过平时练习少每天在 IDE 里手写 1 到 2 道题面试紧张缺乏模拟找朋友或录音模拟面试10.3 项目经历怎么讲面试官听项目核心想确认三点项目是不是你真正做的。你在项目中遇到了什么问题。你是怎么分析、解决并验证的。推荐按 STAR 法则讲背景、任务、行动、结果。尤其是“难点”部分不要只说“技术难”要讲清楚为什么难当时有哪些方案你选了哪个为什么最终效果如何衡量比如介绍缓存穿透问题时可以这样表达项目里用户详情查询量很大大量请求会访问不存在的用户 ID导致数据库压力上升。我先在 Redis 缓存空值解决短期穿透再考虑引入布隆过滤器做前置拦截最终把接口平均响应时间从 120ms 降到 40ms。10.4 8月剩余时间怎么复习如果距离面试只有两到三周不建议再全面铺开。优先按照下面顺序复习先把本文中的高频题逐题过一遍每个问题都能流畅说出答案。每天手写两道算法题重点复习数组、链表、二叉树、动态规划。准备一个完整的项目复盘包括项目架构图、核心表结构、接口流程、难点和优化点。看一遍 JDK 集合源码中的 ArrayList、HashMap再看 Spring 的 Bean 生命周期。找一个人模拟面试至少完整走一遍自我介绍、项目介绍、反问环节。复习过程中如果发现某块知识点完全陌生别慌。先标记下来等整体过完一轮后再回头补面试阶段掌握“高频考点”的优先级永远大于“冷门偏题”。8月份的机会窗口还在把有限的时间花在最有区分度的考点上比盲目刷一堆题库更有价值。