1. 项目概述:为什么我们需要一份“最新”的面试题集?
又到了招聘季,或者说,对于Java开发者而言,招聘季似乎从未真正结束过。最近在帮团队做技术面试官,也和一些猎头、HR朋友交流,发现一个挺有意思的现象:很多候选人简历上项目经验看着不错,但一聊到基础或者一些“新瓶装旧酒”的深度问题,就露怯了。他们手里可能也刷过不少“Java面试宝典”,但那些资料往往版本陈旧,还在纠结JDK 1.7的HashMap死循环,或者对Java 8之后的诸多重要特性语焉不详。这让我意识到,一份真正“有用”的面试题集,绝不仅仅是问题的罗列,它必须紧跟技术发展的脉搏,反映当前一线大厂和优质中小公司的实际考察重点。
“100道最新Java面试题”这个标题,背后对应的是一个非常实际且持续的需求:信息更新与深度挖掘。Java生态庞大且迭代迅速,从语言本身(如Records、Pattern Matching、虚拟线程)到核心框架(Spring Boot 3.x、Spring Cloud 202x),再到基础设施(容器化、云原生、响应式编程),考察维度在不断拓宽和深化。面试官希望用高效的问题筛选出具备扎实基础、持续学习能力和解决实际问题潜力的开发者;而求职者则需要一份精准的“考纲”和“参考答案”,来查漏补缺,系统性地准备。这份题集的价值,就在于它试图弥合这种信息差,将散落在各种面经、官方文档、技术博客中的核心知识点,结合最新的实践,进行系统化的梳理和解读。
它适合谁呢?首先是正在积极寻找Java开发岗位的求职者,无论是应届生还是有一定经验的工程师,都能从中找到对应的准备方向。其次,是那些希望巩固自身知识体系、检验学习成果的开发者。甚至对于面试官而言,这份结构化的题目和解析,也能为设计面试问题、评估候选人水平提供有价值的参考。接下来,我将从设计思路开始,拆解这份题集是如何构建的,并深入其中一些最具代表性的题目,分享我的理解和准备建议。
2. 内容整体设计与思路拆解:如何构建一份不过时的面试指南?
设计一份能称之为“最新”且“常见”的面试题集,绝不是简单地从网上复制粘贴一百个问题。它需要一个清晰的逻辑框架和持续的更新机制。我的核心设计思路围绕三个维度展开:技术栈分层、考察深度分级、场景化关联。
2.1 技术栈分层:构建清晰的知识地图
我将100道题目大致划分为五个核心层次,这模拟了面试中从浅入深、从点到面的考察路径:
Java语言基础与核心API(约25题):这是基石。但“最新”意味着不止于String、集合、多线程的老三样。这里会重点涵盖:
- Java 8+新特性:Lambda表达式、Stream API的实战技巧与性能考量(例如,
parallelStream的使用陷阱);Optional的正确使用姿势,避免get()调用前不检查isPresent()。 - 新版本语言特性:Java 17作为新的LTS版本,其密封类(Sealed Classes)、模式匹配(Pattern Matching for
instanceof和switch)已成为高频考点。虚拟线程(Virtual Threads)作为颠覆性的并发模型,更是必须理解的核心概念。 - 核心API深度:比如,
HashMap在JDK 1.8之后的红黑树优化、扰动函数细节;ConcurrentHashMap在JDK 1.7和1.8中的实现差异,以及为何放弃分段锁。
- Java 8+新特性:Lambda表达式、Stream API的实战技巧与性能考量(例如,
JVM与性能调优(约15题):中级及以上岗位的必考区。这部分内容相对稳定,但考察角度在变化。
- 内存模型(JMM):围绕
volatile、synchronized、happens-before原则,结合最新的内存屏障知识进行提问。 - 垃圾回收器:不仅要知道G1、ZGC、Shenandoah的名字,更要清楚其工作原理、适用场景(如低延迟、大堆),以及如何根据应用特点选择和调优。
- 性能诊断工具:
jstack,jmap,jstat,jcmd的实战用法,以及如何结合Arthas这样的在线诊断工具进行问题定位。
- 内存模型(JMM):围绕
主流框架与生态(约30题):这是权重最高的部分,直接体现工程能力。
- Spring Framework 5.x / 6.x & Spring Boot 2.x / 3.x:重点考察自动配置原理(
spring.factories的演变)、Spring Boot启动过程、AOP的动态代理选择(JDK vs CGLIB)及性能影响、Spring事务传播机制与失效场景。 - Spring Cloud微服务生态:服务发现(Nacos vs Eureka)、配置中心、网关(Spring Cloud Gateway的过滤器链)、熔断降级(Resilience4j或Sentinel)、OpenFeign的通信原理与优化。
- ORM框架(MyBatis/MyBatis-Plus, JPA/Hibernate):N+1查询问题、一级/二级缓存、延迟加载原理及事务边界的影响。
- Spring Framework 5.x / 6.x & Spring Boot 2.x / 3.x:重点考察自动配置原理(
存储与中间件(约20题):数据库和消息队列是系统瓶颈的高发区。
- MySQL/PostgreSQL:索引优化(B+树、覆盖索引、索引下推)、事务隔离级别与锁(行锁、间隙锁、临键锁)、SQL调优(执行计划解读)。
- Redis:数据结构与应用场景(如GeoHash用于地理位置)、持久化方案(RDB/AOF混合)、集群模式(主从、哨兵、Cluster)、缓存穿透/击穿/雪崩的解决方案。
- 消息队列(Kafka, RocketMQ):高可用原理、消息可靠性保证(事务消息、顺序消息)、消费模型(推 vs 拉)与Rebalance机制。
系统设计与工程实践(约10题):面向高级/架构师岗位,考察综合能力。
- 设计模式:不仅是背诵23种模式,更要考察在Spring等框架中如何被应用(如模板方法、策略、观察者模式)。
- 分布式问题:分布式ID生成方案、分布式锁的实现与选型(Redis vs Zookeeper)、分布式事务(Seata的AT/TCC模式)。
- 容器化与云原生:Dockerfile优化、Kubernetes核心概念(Pod, Deployment, Service)、服务网格(Service Mesh)的边车模式。
2.2 考察深度分级:从“知道”到“精通”
每一道题,我都试图设计出不同深度的追问链,这模拟了面试官的追问逻辑:
- Level 1: 概念知晓:是什么?基本用法?(例如:什么是Spring Bean?)
- Level 2: 原理理解:为什么?如何实现的?(例如:Spring Bean的生命周期是怎样的?
@Autowired是如何完成注入的?) - Level 3: 实践应用与调优:怎么用更好?会遇到什么问题?(例如:Bean的循环依赖如何解决?构造器注入和Setter注入如何选择?单例Bean中的成员变量为何要谨慎使用?)
- Level 4: 源码关联与扩展:和哪些其他知识关联?最新发展是什么?(例如:Spring Boot 3.x中,
spring.factories机制被META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件取代,你了解其背后的设计考量吗?)
2.3 场景化关联:从孤立知识点到解决实际问题
避免题目成为孤立的“八股文”。我会将多个知识点串联到一个具体的业务或技术场景中。例如:“在一个高并发的秒杀系统中,如何设计缓存和库存扣减方案?” 这个问题可以牵扯出Redis分布式锁、Lua脚本保证原子性、缓存与数据库的数据一致性方案(先更新数据库还是先删除缓存?)、MQ异步削峰、防止超卖等多个知识点。
注意:这份题集的“答案”部分,我提供的不仅仅是标准定义,更多的是分析思路、权衡取舍和避坑指南。因为在实际面试中,展现思考过程远比背出一个“标准答案”更重要。
3. 核心细节解析与实操要点:拆解几道典型“新”题与“深”题
让我们深入几道题目,看看如何从“新”和“深”两个角度进行准备。
3.1 虚拟线程(Virtual Threads)—— 理解并发的未来式
题目:请解释Java 19+中引入的虚拟线程(Virtual Threads)是什么?它与平台线程(Platform Threads)有何本质区别?在什么场景下使用虚拟线程能带来显著收益?又需要注意哪些问题?
解析与要点: 虚拟线程是Project Loom的核心成果,旨在用更低的资源开销实现高并发。其本质是由JVM管理的轻量级用户态线程,而非操作系统内核线程。
核心区别:
- 平台线程:与操作系统内核线程是1:1映射关系。创建、调度、上下文切换成本高(涉及内核态切换),数量受限于操作系统。
- 虚拟线程:与平台线程是M:N映射关系。大量虚拟线程可以复用在少数几个平台线程(称为载体线程)上执行。其挂起(如等待I/O)和恢复由JVM在用户态调度,成本极低,可以轻松创建数百万个。
收益场景:
- 高并发I/O密集型应用:这是虚拟线程的主场。例如,Web服务器(如Spring MVC/WebFlux)、微服务网关、数据库连接池等,这些场景下线程大部分时间在等待网络或磁盘I/O。使用虚拟线程可以近乎1:1地按照请求数创建线程,大幅提升系统吞吐量,而无需复杂的异步回调或响应式编程模型。
- 简化并发编程模型:你可以继续使用熟悉的
Thread、ExecutorService和同步阻塞的代码风格(如HttpClient.send()),就能获得媲美异步非阻塞的性能,降低了心智负担和代码复杂度。
注意事项与避坑指南:
- CPU密集型任务不适用:如果任务是纯CPU计算,虚拟线程被挂起的机会少,其优势无法发挥,反而可能因为调度带来轻微开销。此时使用平台线程池更合适。
- 线程局部变量(ThreadLocal)需谨慎:虚拟线程成本低,可能被大量创建。如果每个虚拟线程都持有昂贵的
ThreadLocal资源(如数据库连接),会导致资源快速耗尽。应考虑使用ScopedValue(Java 20+预览)或其他资源管理方式。 - 同步原语(Synchronized)可能阻塞载体线程:在
synchronized块或ReentrantLock内部执行可能挂起的操作(如I/O),会阻塞其所在的载体线程,从而影响该载体线程上运行的其他虚拟线程。应优先使用java.util.concurrent包中的锁,如ReentrantLock的newCondition().await(),这些锁被设计为感知虚拟线程,会在等待时释放载体线程。 - 不是所有框架都已完美适配:虽然Spring Framework 6.x和Spring Boot 3.x已原生支持虚拟线程(通过配置
spring.threads.virtual.enabled=true),但一些底层库或驱动程序可能还有兼容性问题,需要在实际生产环境中充分测试。
实操心得:在测试环境中,我们可以用一个简单的例子对比。创建一个执行HTTP请求的任务,分别用固定大小为200的平台线程池和虚拟线程(Executors.newVirtualThreadPerTaskExecutor())来执行10,000次。你会发现,虚拟线程方案在资源消耗(内存、CPU)和完成时间上通常有数量级的优势,尤其是在后端服务响应较慢的情况下。
3.2 Spring Boot自动配置原理的深度追问
题目:请详细描述Spring Boot自动配置(Auto-configuration)的工作原理。从@SpringBootApplication注解开始,到一份配置类生效,中间经历了哪些关键步骤?如何自定义一个Starter并实现自动配置?
解析与要点: 这是一道经典题,但“最新”的考察点在于Spring Boot 2.7之后到3.0的演进。
- 启动入口:
@SpringBootApplication是一个复合注解,包含@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。核心是@EnableAutoConfiguration。 - 加载自动配置类(关键变化点):
- Spring Boot 2.7及之前:主要通过
META-INF/spring.factories文件,在org.springframework.boot.autoconfigure.EnableAutoConfiguration键下列出全限定类名。SpringFactoriesLoader负责加载。 - Spring Boot 3.0及之后:改为使用
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。每行写一个自动配置类的全限定名。这是为了支持GraalVM原生镜像的提前编译(AOT),因为spring.factories的动态加载机制不利于AOT分析。
- Spring Boot 2.7及之前:主要通过
- 条件化装配:加载的配置类不会全部生效。Spring Boot大量使用
@Conditional系列注解(如@ConditionalOnClass,@ConditionalOnMissingBean,@ConditionalOnProperty)进行判断。例如,DataSourceAutoConfiguration只在类路径下存在DataSource类且用户未自定义DataSourceBean时才会生效。 - 执行顺序:自动配置类按
@AutoConfigureOrder或@AutoConfigureBefore/@AutoConfigureAfter指定的顺序执行,确保依赖关系正确。
- 自定义Starter实操步骤:
- 创建一个独立的Maven/Gradle项目,命名规范为
xxx-spring-boot-starter。 - 添加对
spring-boot-autoconfigure和spring-boot-configuration-processor的依赖。 - 编写你的核心服务类(如
MyService)。 - 编写自动配置类
MyAutoConfiguration,使用@Configuration注解,并在类上添加@ConditionalOnClass(MyService.class)等条件注解。在配置类中定义@Bean方法来创建MyService实例。 - 在
resources/META-INF/spring/目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(Boot 3.x+),内容为你的自动配置类全名。如果是Boot 2.7,则需创建spring.factories文件。 - (可选)使用
@ConfigurationProperties绑定外部配置(如application.yml中的my.service.config),提供类型安全的配置。
- 创建一个独立的Maven/Gradle项目,命名规范为
注意:在自定义自动配置时,务必遵循“默认最优,开箱即用,允许覆盖”的原则。即提供合理的默认值,同时通过条件注解确保用户自定义的Bean能优先被使用。
4. 实操过程与核心环节实现:以“分布式锁”场景为例的深度剖析
让我们以一个综合性极强的场景——“实现一个高可用的分布式锁”为例,将多个模块的知识点串联起来,并给出可落地的实操方案。
4.1 场景定义与需求分析
假设我们有一个商品库存扣减服务,部署在多个实例上。为了防止超卖,在扣减库存前,必须对商品ID进行加锁,确保同一时间只有一个请求能执行扣减逻辑。
核心需求:
- 互斥性:在分布式环境下,同一时刻只有一个客户端能持有锁。
- 避免死锁:锁必须有超时机制,即使持有锁的客户端崩溃,锁也能自动释放。
- 高可用:锁服务本身需要高可用,不能成为单点故障。
- 高性能:加锁、释放锁的操作延迟要低,不能成为系统瓶颈。
4.2 方案选型与对比
常见的分布式锁实现有三种,各有优劣:
| 实现方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 基于Redis | 使用SET key value NX PX timeout命令。NX保证只有key不存在时设置成功(互斥),PX设置毫秒级超时(避免死锁)。 | 性能极高,实现简单,社区方案成熟(如Redisson)。 | 非强一致性。主从异步复制下,主节点宕机可能导致锁丢失(即使使用RedLock也有争议)。 | 对性能要求极高,可以容忍极端情况下少量锁失效的场景(如秒杀库存有最终校验)。 |
| 基于ZooKeeper | 创建临时有序节点。每个客户端在锁节点下创建临时顺序节点,判断自己是否是最小节点,是则获锁。监听前一个节点,锁释放后得到通知。 | 利用ZK的强一致性和临时节点特性,可靠性高,锁自动释放。 | 性能相对Redis较低,依赖ZK集群,增加了运维复杂度。 | 对锁的可靠性要求极高,且并发量不是特别巨大的场景。 |
| 基于数据库 | 利用数据库的唯一约束或乐观锁版本号。 | 实现简单,无需引入新组件。 | 性能差,数据库压力大,锁失效处理复杂(如行锁长时间不释放)。 | 基本不推荐用于高并发场景,仅作为备选或理解原理用。 |
实操选择:对于大多数互联网应用,基于Redis的实现是首选,因其在性能、复杂度和可靠性之间取得了较好的平衡。我们选择使用Redisson框架,它封装了看门狗(Watchdog)自动续期、可重入锁、联锁、红锁等高级特性,避免了手动实现的许多坑。
4.3 基于Redisson的实操实现
引入依赖:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.27.0</version> </dependency>配置Redisson客户端(以单节点为例,生产环境建议用集群或哨兵模式):
# application.yml spring: redis: host: localhost port: 6379 # password: your-password database: 0Redisson-Starter会自动根据Spring Redis配置创建客户端。
编写锁服务工具类:
import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class DistributedLockService { @Autowired private RedissonClient redissonClient; /** * 尝试加锁,指定等待时间和锁持有时间 * @param lockKey 锁的键,如 "stock_lock:{productId}" * @param waitTime 最大等待时间(秒) * @param leaseTime 锁自动释放时间(秒),-1表示使用看门狗自动续期 * @return 是否成功获取锁 */ public boolean tryLock(String lockKey, long waitTime, long leaseTime) { RLock lock = redissonClient.getLock(lockKey); try { return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 return false; } } /** * 释放锁 * @param lockKey 锁的键 */ public void unlock(String lockKey) { // 注意:确保由加锁的线程来解锁 RLock lock = redissonClient.getLock(lockKey); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }在业务中使用:
@Service public class StockService { @Autowired private DistributedLockService lockService; @Autowired private StockMapper stockMapper; public boolean deductStock(Long productId, Integer quantity) { String lockKey = "stock_lock:" + productId; boolean locked = false; try { // 尝试获取锁,最多等待3秒,锁持有10秒后自动释放(看门狗会在业务未完成时自动续期) locked = lockService.tryLock(lockKey, 3, 10); if (!locked) { throw new RuntimeException("系统繁忙,请稍后重试"); } // 执行核心库存扣减业务逻辑 Stock stock = stockMapper.selectByIdForUpdate(productId); // 使用select ... for update 或 乐观锁 if (stock.getAvailable() >= quantity) { stock.setAvailable(stock.getAvailable() - quantity); stockMapper.updateById(stock); // ... 其他业务逻辑 return true; } else { return false; } } finally { // 确保锁被释放 if (locked) { lockService.unlock(lockKey); } } } }
4.4 核心环节与原理解读
看门狗(Watchdog)机制:这是Redisson避免死锁的核心。当你获取锁时,如果不指定
leaseTime或设置为-1,Redisson会启动一个看门狗线程。它默认每10秒(锁超时时间的1/3)检查一次业务是否还在执行(即锁是否还被持有),如果是,则自动将锁的超时时间重置为初始值(默认30秒)。这保证了只要业务线程还在运行,锁就不会因为超时而被意外释放。业务执行完毕必须手动调用unlock(),这会停止看门狗并释放锁。可重入性:Redisson的
RLock实现了Java的Lock接口,支持同一个线程多次加锁(重入),锁内部有一个计数器,每次lock()加1,unlock()减1,减到0时才真正释放Redis中的锁。这符合Java开发者的使用习惯。锁续期与释放的原子性:Redisson使用Lua脚本在Redis上执行锁续期和释放操作,保证了多个命令的原子性,这是手动使用
SETNX和EXPIRE组合命令无法做到的。
实操心得:
- 锁粒度要细:锁的Key应精确到具体的数据资源,如
stock_lock:1001,而不是全局锁stock_lock,以提高并发度。 - 加锁与解锁必须成对出现:务必在
finally块中释放锁,确保异常情况下锁也能被释放。 - 避免在锁内执行耗时操作或外部调用:锁的持有时间应尽可能短,只覆盖最小的必要临界区。长时间持有锁会降低系统吞吐量,并增加看门狗续期的压力。
- Redis集群模式的选择:生产环境建议使用Redis Cluster或哨兵模式,并合理配置
redisson.yaml中的retryInterval、retryAttempts等参数,以适应网络波动。
5. 常见问题与排查技巧实录:面试与实战中的高频“坑点”
在准备面试和实际开发中,有些问题出现的频率极高。这里我整理了一份“避坑指南”,涵盖从JVM到框架使用的各个方面。
5.1 JVM与并发相关
问题:线上应用CPU占用率突然飙升到100%,如何快速定位问题线程和代码?
- 排查技巧:
- 定位进程:
top -Hp [pid]查看该Java进程下所有线程的CPU占用,找到占用最高的线程ID(十进制)。 - 线程ID转换:将十进制的线程ID转换为十六进制(可以用
printf "%x\n" [tid])。 - 查看线程栈:使用
jstack [pid] > thread_dump.log导出线程堆栈,在文件中搜索刚才转换的十六进制线程ID(nid=0x...),找到对应的线程堆栈信息,即可看到正在执行的代码行。 - 结合其他工具:
jstack看到的是瞬时状态。对于周期性CPU高,可以使用Arthas的thread -n 3 -i 1000命令,持续采样统计最繁忙的线程。或者使用async-profiler生成火焰图,直观展示CPU时间消耗在哪些方法上。
- 定位进程:
- 常见原因:死循环、频繁的GC(Full GC)、锁竞争激烈(大量线程处于
BLOCKED状态)。
- 排查技巧:
问题:应用出现
OutOfMemoryError: Java heap space,但堆内存设置得很大,可能是什么原因?- 排查技巧:
- 确认错误类型:首先区分是堆内存溢出还是方法区(元空间)溢出(
OutOfMemoryError: Metaspace)。 - 分析堆转储:在JVM启动参数中添加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof,让JVM在OOM时自动生成堆转储文件。使用MAT或JVisualVM分析该文件。 - 重点排查:
- 内存泄漏:查看
Histogram或Dominator Tree,找到占用内存最大的对象类型,检查其GC Roots引用链,看是否有意外的强引用(如静态集合类持续添加对象)。 - 不合理的对象分配:一次性加载超大文件到内存、查询数据库未分页导致返回海量数据对象。
- 内存泄漏:查看
- 确认错误类型:首先区分是堆内存溢出还是方法区(元空间)溢出(
- 实操心得:设置合理的堆大小(
-Xms和-Xmx)很重要,但更重要的是优化代码。对于缓存,要设置大小限制和过期策略;对于大集合操作,考虑流式处理或分页。
- 排查技巧:
5.2 Spring框架相关
问题:
@Transactional注解声明式事务失效的常见场景有哪些?- 排查清单:
- 方法非public:
@Transactional只能用于public方法上。 - 自调用问题:在同一个类中,一个非事务方法A调用事务方法B,事务不会生效。因为事务是基于AOP代理的,自调用不走代理。
- 异常类型未被捕获:默认只对
RuntimeException和Error回滚。如果抛出了Exception,需要配置@Transactional(rollbackFor = Exception.class)。 - 数据库引擎不支持:如MySQL的MyISAM引擎不支持事务。
- 传播行为设置不当:例如,在已有事务的方法中调用
@Transactional(propagation = Propagation.NOT_SUPPORTED)的方法,后者会在非事务环境中执行。
- 方法非public:
- 解决方案:对于自调用问题,可以将事务方法抽取到另一个Service中,通过注入调用;或者(不推荐)通过
AopContext.currentProxy()获取当前代理对象再调用。
- 排查清单:
问题:Spring Bean的循环依赖是如何解决的?三级缓存具体指什么?
- 深度解析:这是Spring面试的经典难题。Spring通过三级缓存解决Setter注入和字段注入的循环依赖(构造器注入无法解决)。
- 一级缓存(单例池)
singletonObjects:存放完全初始化好的Bean。 - 二级缓存(早期曝光对象)
earlySingletonObjects:存放提前曝光的、尚未完成属性填充和初始化的Bean(半成品)。 - 三级缓存(对象工厂)
singletonFactories:存放创建Bean的工厂ObjectFactory,用于在需要时生成早期引用(可能经过AOP代理)。
- 一级缓存(单例池)
- 解决流程:以A依赖B,B依赖A为例。
- 创建A,实例化后,将A的工厂放入三级缓存。
- 为A填充属性B,发现B不存在,开始创建B。
- 创建B,实例化后,将B的工厂放入三级缓存。
- 为B填充属性A,从三级缓存中找到A的工厂,通过工厂获取A的早期引用(可能是代理对象),放入二级缓存,并从三级缓存移除A的工厂。将A的早期引用注入给B。
- B完成初始化,放入一级缓存。
- A得到B的实例,完成属性填充和初始化,放入一级缓存,并从二级缓存移除。
- 关键点:三级缓存的核心价值在于处理AOP代理。如果Bean需要被代理,工厂能确保返回的是代理对象,而不是原始对象,保证依赖注入的一致性。
- 深度解析:这是Spring面试的经典难题。Spring通过三级缓存解决Setter注入和字段注入的循环依赖(构造器注入无法解决)。
5.3 数据库与缓存相关
问题:MySQL索引失效的常见场景?
- 速查表:
场景 原因 示例 违反最左前缀原则 复合索引 (a, b, c),查询条件只用b和c。WHERE b=1 AND c=2在索引列上做计算、函数或类型转换 索引存储的是原始值。 WHERE YEAR(create_time)=2023(应为WHERE create_time >= '2023-01-01')使用 !=或<>范围太大,优化器可能认为全表扫描更快。 WHERE status != 1使用 OR连接非索引列如果 OR两边的列不都有索引,会导致全表扫描。WHERE a=1 OR b=2(只有a有索引)LIKE以通配符开头%value%或%value无法利用索引。WHERE name LIKE '%张%'字符串索引未加引号 发生隐式类型转换。 WHERE id = '123'(id是字符串类型,但查询用了数字123) - 排查工具:养成使用
EXPLAIN或EXPLAIN ANALYZE查看SQL执行计划的习惯,关注type(访问类型,index/range以上才好)、key(使用的索引)、rows(预估扫描行数)。
- 速查表:
问题:如何保证Redis与数据库的双写一致性?
- 这是一个复杂问题,没有银弹,需要根据业务对一致性的要求级别来权衡。
- 常见方案对比:
方案 操作顺序 优点 缺点 适用场景 先更新数据库,再删除缓存 1. 更新DB 2. 删除Cache 实现简单,出现不一致的概率较低(缓存过期或下次读时重建)。 删除缓存可能失败,导致脏数据。 最常用,对一致性要求不是极端严格的场景。可结合消息队列重试删除。 先删除缓存,再更新数据库 1. 删除Cache 2. 更新DB 逻辑清晰。 并发下问题严重:A删缓存 -> B读缓存未命中读DB旧值 -> B写缓存旧值 -> A更新DB,导致缓存一直是旧数据。 不推荐单独使用。 延迟双删 1. 删除Cache 2. 更新DB 3. 延迟一段时间再删Cache 缓解方案二的并发问题。 延迟时间难以确定,降低吞吐量。 可作为方案一的增强版。 订阅数据库Binlog 数据库更新 -> 监听Binlog -> 更新/删除缓存 解耦业务代码,一致性高。 系统复杂度高,需要引入Canal、Debezium等中间件。 对一致性要求极高,且技术架构较复杂的系统。 - 个人建议:对于绝大多数互联网业务,采用“先更新数据库,再删除缓存”,并结合以下策略:
- 设置合理的缓存过期时间,作为最终兜底。
- 对于极少数核心数据,可以将缓存更新操作放入消息队列,确保最终成功。或者使用
SETNX实现一个简单的分布式锁,在更新时防止并发读请求读到旧数据并回写缓存(但这会降低性能)。 - 接受在极短时间窗口内可能存在的不一致,通过产品逻辑进行规避(如重要操作后强制刷新页面)。
这份“100道最新Java面试题”的梳理和深度解析,其目的远不止于应付一次面试。它更像是一张技术地图,帮你系统性地审视自己的Java知识体系,发现盲区,建立连接。技术学习永无止境,面试题也只是某个时间点的快照。真正的价值在于,通过准备这些问题的过程,培养起你深入探究原理的习惯、串联知识的能力以及解决复杂问题的系统性思维。当你在面试中,不仅能流畅回答“是什么”,更能清晰阐述“为什么”和“怎么选”,甚至能指出当前方案的局限性和演进方向时,你就已经超越了绝大多数竞争者。最后,保持对新技术的好奇心,坚持动手实践,将学到的知识应用到自己的项目或开源贡献中,这才是工程师成长的硬道理。