面试题
Java基础
Java 类加载过程
Java 类的加载是 Java 虚拟机(JVM)运行的核心机制之一,它负责将 .class 文件加载到内存,并通过一系列步骤完成链接和初始化,使得类可以被使用。
Java 类加载过程
Java 的类加载主要包含 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization) 五个阶段:
1. 加载(Loading)
- 找到并读取
.class文件(字节码)。 - 创建
java.lang.Class对象,用于存放该类的元信息。 - 将字节码内容解析并存入 JVM 运行时数据区的「方法区」。
🚀 执行过程
- 通过 类加载器(ClassLoader) 读取
.class文件内容。 - 通过 字节码解析 生成
Class类对象,并存放在方法区。
2. 验证(Verification)
- 主要 检查 .class 文件的正确性、安全性、合规性,防止非法操作导致 JVM 崩溃。
- 包括:
- 文件格式校验(是否符合 Class 文件规范,如魔数、版本)
- 元数据校验(类的继承结构是否符合规范)
- 字节码校验(防止非法操作,如对象操作是否符合类型要求)
- 符号引用校验(类、字段、方法等是否存在)
3. 准备(Preparation)
- 准备阶段会:
- 为类的静态变量分配内存,并设置默认初始值(不会执行赋值语句!)。
- 这个 内存是分配在方法区的(JDK 8 以前)。
🌟 示例
public class Test {
static int a = 10;
}
执行 准备阶段 时:
static int a = 0; // **仅初始化默认值,不会赋值 10**
赋值 a = 10 是 初始化阶段 执行的!
4. 解析(Resolution)
- 将 符号引用 转换为 直接引用:
- 符号引用:在
.class文件中使用的符号(如字符串"java/lang/Object")。 - 直接引用:已经在 JVM 运行时数据区中指向目标地址的引用。
- 符号引用:在
💡 示例
假设 Test 类调用:
Test t = new Test();
解析阶段 将 "Test" 这个符号引用解析为内存地址的直接引用。
5. 初始化(Initialization)
- 执行类的静态变量赋值 和 静态代码块。
- 只有在类主动使用时才会触发初始化。
🚀 示例
public class Test {
static int a = 10;
static {
a = 20;
System.out.println("Test initialized");
}
}
初始化阶段
- 先赋值
a = 10 - 再执行静态代码块
a = 20 - 输出
Test initialized
🛑 什么时候触发初始化?
- 创建实例:
new Test() - 访问静态变量:
System.out.println(Test.a) - 调用静态方法:
Test.method() - 使用 反射:
Class.forName("Test") - JVM 启动时加载
main所在类
类加载器(ClassLoader)
Java 采用 双亲委派模式(Parent Delegation Model) 进行类加载:
- Bootstrap ClassLoader(引导类加载器)
- 加载
java.lang.*核心 API(如Object、String、System)。
- 加载
- Extension ClassLoader(扩展类加载器)
- 加载
ext目录下的类库(如javax)。
- 加载
- Application ClassLoader(应用类加载器)
- 加载 CLASSPATH 下的类(如
classpath中的.class文件)。
- 加载 CLASSPATH 下的类(如
- 自定义类加载器
- 继承
ClassLoader,可 自定义加载策略。
- 继承
双亲委派模型
工作机制
- 若
ClassLoader需要加载某个类,它会先 委托父类加载器 尝试加载; - 只有当 父加载器无法找到该类,才会由自身去加载。
目的
✅ 避免类的重复加载(保证 Java 核心类不会被篡改)
✅ 提高安全性和稳定性(防止恶意代码伪造 java.lang.String 类)
🚀 示例
假设一个自定义类 MyClass:
Class<?> clazz = Class.forName("com.example.MyClass");
访问流程
Application ClassLoader先 让ExtClassLoader试图加载。ExtClassLoader进一步委托Bootstrap加载。Bootstrap未找到该类,因此Ext也返回失败。Application ClassLoader最终自己加载类。
破坏双亲委派模式
在一些特定场景下(如 热部署 和 插件机制),可能 打破双亲委派规则:
- Tomcat (Web 容器):每个 WebApp 需要隔离 ClassLoader,避免类冲突。
- SPI(Service Provider Interface):需要让某些类优先被自定义类加载器加载。
💡 破坏方式
- 直接继承
ClassLoader,重写findClass(),绕过父类加载器:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
}
💡 调用 defineClass() 手动加载字节码,实现 按需定制类加载。
总结
-
类的生命周期
- 加载(Loading)→ 验证(Verification) → 准备(Preparation) → 解析(Resolution) → 初始化(Initialization) → 使用 → 卸载
-
类加载器
- Bootstrap(加载 Java 核心类)
- Extension(加载
ext目录中 JAR) - Application(加载 CLASSPATH 下的类)
- 自定义 ClassLoader
-
双亲委派模型
- 父类优先加载,防止重复加载
- 可 自定义破坏双亲委派(如 Tomcat、SPI 机制)
➕ 深入掌握类加载机制,有助于理解 Java 的动态性和插件化开发! 🚀
Java 集合 & JDK 源码
1. Java 容器框架的整体结构
问题:Java 容器(Collection Framework)有哪些主要接口?
Java 集合框架主要分为 Collection 和 Map 体系:
-
Collection 接口
- List(有序、可重复):ArrayList、LinkedList、Vector
- Set(无序、不重复):HashSet、LinkedHashSet、TreeSet
- Queue(队列):PriorityQueue、LinkedList(Deque)
- Deque(双端队列):ArrayDeque
-
Map 接口(键值对)
- HashMap(无序)
- LinkedHashMap(插入顺序)
- TreeMap(红黑树,按 key 排序)
- ConcurrentHashMap(并发安全)
2. ArrayList 和 LinkedList 的区别
问题:ArrayList 和 LinkedList 有哪些区别?
| 方面 | ArrayList | LinkedList |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 插入/删除 | 低效(需要移动数据 O(n)) | 高效(O(1) 仅修改指针) |
| 随机访问 | 高效(O(1)) | 低效(O(n),需要遍历链表) |
| 占用空间 | 较小 | 较大(存储额外指针) |
| 线程安全 | 不安全 | 不安全 |
结论:
- 查询操作多 → ArrayList
- 频繁插入、删除 → LinkedList
3. HashMap 的底层实现(JDK 1.8)
HashMap 的数据结构与原理?
- HashMap 本质是 数组 + 链表 + 红黑树
- JDK 1.8 进行了优化:
- 当链表长度 ≥ 8 时,会转换为红黑树,以提高搜索效率
- 采用优化的 rehash 机制,避免额外性能开销
核心方法实现
1)put() 方法
存储 key-value:
- 计算 key 的 hash 值 并确定 桶索引
- 如果索引处为空,则直接放入
- 如果索引处有值:
- 采用链表存储冲突元素(JDK 1.8 以上,大于 8 个元素转换成红黑树)
- 扩容机制:
- 负载因子(默认 0.75)
- 容量达到 threshold 时,扩展 2 倍
2)get() 方法
通过 key 查找 value:
- 计算 hash 值,定位到桶索引
- 遍历链表 / 红黑树,匹配 key,返回 value
为什么 HashMap 的 key 需要重写 equals() 和 hashCode()?
- hashCode() 确定 桶索引(减少 hash 冲突)
- equals() 确保 key 的 唯一性
红黑树
红黑树(Red-Black Tree,RBT)是一种 自平衡二叉搜索树(Self-Balancing Binary Search Tree),它保证树的高度近似平衡,以确保插入、删除、查找操作的时间复杂度稳定在 O(log n) 。
在 Java 中,红黑树主要应用在:
- TreeMap(Java 集合框架)
- TreeSet(底层基于 TreeMap 实现)
- ConcurrentSkipListMap
- JDK 1.8 之后的 HashMap(当链表长度超过 8 时转为红黑树,以提升查找效率)
特性:红黑树是一种二叉搜索树(BST),但比普通 BST 增加了几个额外的性质,使得它在频繁插入和删除时仍能高效运作。
红黑树必须满足以下 5 个性质:
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色。
- 所有叶子节点(NIL,空节点)都是黑色的。
- 如果一个节点是红色,则它的两个子节点必须是黑色(即红色节点不能连续相连)。
- 从任一节点到其所有叶子节点的所有路径上,黑色节点的个数必须相同(黑色平衡)。
由于这些限制,红黑树的 最长路径不会超过最短路径的 2 倍,保证了查找操作的时间复杂度约为 O(log n)。
红黑树的操作
1. 插入(Insert)
当插入新节点时,默认新节点为红色,以保持黑色平衡。插入后可能会导致红黑树性质被破坏,因此需要进行调整,主要通过 颜色变换、左旋转和右旋转 操作恢复平衡。
插入后的调整分为以下几种可能情况:
-
Case 1:新节点的父节点是黑色
- 不需要调整,红黑树性质依然满足。
-
Case 2:父节点是红色,叔叔节点也是红色
- 解决办法:颜色翻转(Color Flip)
- 将父节点和叔叔节点染成黑色,将祖父节点染成红色,并继续向上检查祖父节点是否违反规则。
- 解决办法:颜色翻转(Color Flip)
-
Case 3:父节点是红色,叔叔节点是黑色
- 根据新节点是左子节点还是右子节点,分为两类情况:
- 左子节点:右旋转
- 右子节点:先左旋,再右旋
- 根据新节点是左子节点还是右子节点,分为两类情况:
2. 旋转(Rotation)
旋转是保持红黑树平衡的重要操作,有 左旋和右旋 两种。
左旋(Left Rotation)
-
发生在 右子树较重 的情况(右子节点成为根)。
-
例子:
A \ B \ C左旋后变为:
B / \ A C
右旋(Right Rotation)
-
发生在 左子树较重 的情况(左子节点成为根)。
-
例子:
C / B / A右旋后变为:
B / \ A C
3. 删除(Delete)
删除操作相对复杂:
-
删除叶子节点(无子节点):
- 如果是 红色节点,直接删除。
- 如果是 黑色节点,可能破坏黑色平衡,需要调整(删除后可能要进行旋转)。
-
删除带单个子节点的节点
- 用其子节点替换自己,保持树结构。
-
删除带两个子节点的节点
- 找到后继节点(通常是右子树的最小节点),替换后再删除。
删除后,可能违反红黑树性质,因此需要 重新调整颜色和旋转。
红黑树 vs AVL 树
| 数据结构 | 自平衡 | 时间复杂度(查找、插入、删除) | 旋转次数 | 适合场景 |
|---|---|---|---|---|
| 红黑树 | 近似平衡 | O(log n) | 最多 2 次旋转 | 插入、删除较多 |
| AVL 树 | 严格平衡 | O(log n) | 最多 4 次旋转 | 查找操作频繁 |
红黑树更适用于 插入、删除 频繁的场景,如 操作系统调度、数据库索引、Java TreeMap 等,而 AVL 树更适用于对 查找要求更快 的场景。
红黑树的实际应用
-
Linux 进程调度(CFS 调度器)
- Linux 内核中,红黑树被用来管理 进程调度时间片(Completely Fair Scheduler, CFS)。
-
MySQL InnoDB 事务(Adaptive Hash Index)
- MySQL InnoDB 存储引擎的 自适应哈希索引 可能会采用红黑树进行索引优化。
-
Java Collections Framework
- TreeMap(红黑树实现,保证 key 的有序性)
- TreeSet(基于 TreeMap 实现,去重且有序)
-
JDK 1.8 HashMap
- 当 链表长度超过 8 时,会自动转化为红黑树,降低查找复杂度 O(n) → O(log n)。
总结
- 红黑树通过额外的颜色属性和旋转操作有效保持平衡,确保树的高度接近
log(n),从而保证高效的增删查改操作(时间复杂度O(log n))。 - 通过左旋、右旋、颜色翻转,红黑树在插入和删除时能动态保持平衡。
- 由于红黑树在插入和删除时最多旋转两次,适用于高效的增删查场景,比如 Linux CFS 调度、Java TreeMap、MySQL 索引等。
- 与 AVL 树相比,红黑树插入删除成本更低,而 AVL 更适用于查找密集场景。
4. HashMap 在并发下的问题以及解决方案
问题:在多线程环境下使用 HashMap 有哪些风险?
并发问题:
- 死循环(JDK 1.7)
- 由于头插法导致链表环,形成死循环
- 数据丢失
- 并发 put 可能导致数据丢失
解决方案
| 方案 | 解决方式 |
|---|---|
| 使用线程安全 容器 | ConcurrentHashMap |
| 加锁 | Collections.synchronizedMap() |
| 读写分离 | ReadWriteLock |
5. ConcurrentHashMap 的底层原理
ConcurrentHashMap 和 HashMap 的区别?
| 方面 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(分段锁或 CAS) |
| 默认数据结构 | 数组 + 链表 + 红黑树 | 数组 + 链表 + 红黑树 |
| 锁机制 | 无 | JDK 1.7 分段锁 / JDK 1.8 CAS |
| 并发性能 | 低,在多线程下可能出错 | 高,支持高并发读取 |
ConcurrentHashMap 1.8 主要优化
- 去掉 Segment 分段锁,采用 CAS + Synchronized
- bucket 采用 链表 + 红黑树
6. HashSet 的底层实现
HashSet 为什么不允许元素重复?
- 底层基于 HashMap
add(E e)方法底层调用map.put(e, PRESENT),其中 PRESENT 是一个static final Object- 由于 HashMap 的 key 不能重复,所以 HashSet 也不能重复
7. TreeMap 和 TreeSet 的实现
TreeMap 和 TreeSet 有什么区别?
| 方面 | TreeMap | TreeSet |
|---|---|---|
| 底层数据结构 | 红黑树 | 红黑树 |
| 排序 | 按 key 排序 | 按元素排序 |
| 内部存储 | 键值对(K-V) | 仅存储 key |
TreeMap 默认按照 key 的自然顺序(Comparable) 排序,也可使用 Comparator 自定义排序规则。
8. LinkedHashMap 的原理
LinkedHashMap 如何保证有序?
- 继承自 HashMap
- 维护了 双向链表,记录元素插入顺序
- 可用于 LRU缓存实现(
removeEldestEntry()方法)
9. WeakHashMap 与 HashMap 的区别
WeakHashMap 的特点?
| 方面 | HashMap | WeakHashMap |
|---|---|---|
| 键的类型 | 强引用 Key | 弱引用 Key |
| 内存回收 | 需要手动删除 | GC 自动回收 |
| 适用场景 | 普通缓存 | 缓存/临时对象 |
当 GC 发现 WeakHashMap 中的 key 没有强/软引用时,对象会被自动清除。
10. CopyOnWriteArrayList 的实现
CopyOnWriteArrayList 如何保证线程安全?
- 写操作时,创建新数组(CopyOnWrite)
- 适用于 读多写少 的场景,例如 缓存
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(10); // 创建新数组并复制数据
总结
| 数据结构 | 底层实现 | 特点 |
|---|---|---|
| ArrayList | 动态数组 | 查询快,插入慢 |
| LinkedList | 双向链表 | 插入删除快,查询慢 |
| HashMap | 数组 + 链表 + 红黑树 | 无序,不安全 |
| ConcurrentHashMap | CAS + 链表 + 红黑树 | 线程安全,并发性能高 |
| TreeMap | 红黑树 | 按 key 排序 |
| LinkedHashMap | HashMap + 双向链表 | 有序(按插入顺序) |
| WeakHashMap | 弱引用 HashMap | GC 自动回收 |
| CopyOnWriteArrayList | 数组 + 写时复制 | 读多写少 |
拦截器和过滤器区别
在 Java Web 或 Spring 开发中,拦截器(Interceptor) 和 过滤器(Filter) 都用于对请求进行预处理或后处理,但它们有一些关键的区别:
1. 定义和作用范围
| 对比项 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 定义 | Servlet 规范提供的组件(javax.servlet.Filter),用于对请求、响应和资源进行过滤 |
Spring 框架提供的机制(HandlerInterceptor),用于拦截 Spring MVC 的请求处理 |
| 作用范围 | 作用于整个 Web 应用,能够拦截所有请求(如静态资源、JSP、Servlet 等) | 只拦截Spring MVC 处理的请求(即经过 DispatcherServlet 的请求) |
| 依赖性 | 与 Spring 无关,属于 Java EE 规范 | 依赖于 Spring 框架,专用于拦截 Spring MVC 处理的请求 |
2. 执行顺序
| 执行流程 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 调用阶段 | 在请求进入后,DispatcherServlet 之前执行 |
DispatcherServlet 解析请求后,执行 HandlerMapping 解析处理器后执行 |
| 执行顺序 | 多个 Filter 按 web.xml 或 @Filter 注解的顺序执行 |
多个 Interceptor 按 addInterceptors(Spring 配置)注册的顺序执行 |
| 执行方式 | 基于链式调用(Chain),通过 filterChain.doFilter(request, response) 传递控制权 |
依赖 Spring MVC 机制,在 preHandle() 返回 false 时可中断请求 |
3. 生命周期方法
| 生命周期方法 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 初始化 | init(FilterConfig filterConfig) 方法 |
AfterPropertiesSet(Spring 容器加载完毕后执行) |
| 请求前 | doFilter(ServletRequest request, ServletResponse response, FilterChain chain) |
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) |
| 请求后 | 在 doFilter 之后执行(可能影响静态资源) |
postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) |
| 完成后 | destroy() 方法(在 Filter 销毁时执行) |
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) |
4. 主要应用场景
| 场景 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 通用功能 | 可以用于所有请求,如静态资源(CSS、JS)、Servlet | 只能用于MVC 请求 |
| 请求日志 | ✅ 记录所有请求的日志 | ✅ 记录 SpringMVC 相关请求的日志(更精准) |
| 用户认证 | ✅ 可用于登录校验,但需要自己解析请求 | ✅ 更适合用户权限认证,可直接访问 HandlerMethod |
| 跨域处理 | ✅ 适合 CORS 处理 | ❌ 不适用于静态资源的跨域处理 |
| 编码过滤 | ✅ 适用于所有请求的编码(如 CharacterEncodingFilter) |
❌ |
| 拦截特定业务逻辑 | ❌ 需要手动判断是否属于业务逻辑请求 | ✅ 适用于 SpringMVC 业务逻辑处理,如权限判断 |
6. 总结
| 维度 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 适用场景 | 全局过滤,如日志、编码、跨域 | 业务逻辑处理,如权限校验 |
| 依赖性 | 属于 Servlet 规范 | 需依赖 Spring |
| 执行顺序 | 先于 DispatcherServlet |
在 HandlerMapping 之后 |
| 作用请求 | 适用于所有请求 | 仅适用于 Controller 层 |
| 适用范围 | 适用于整个 Web 项目 | 适用于 Spring MVC |
选用建议
- 如果想控制所有请求(包括静态资源、非 MVC 请求):使用
Filter - 如果仅需拦截 Spring MVC 请求(Controller 级别):使用
Interceptor - 如果处理涉及业务逻辑(如权限校验):建议使用
Interceptor - 如果只想处理请求链优化(日志、编码等):使用
Filter
希望这篇详细对比能帮助你理解 Filter 和 Interceptor 的区别 😊
多线程安全问题
在 Java 多线程编程中,线程安全问题主要涉及 原子性、可见性、有序性。下面我们详细分析这些问题,并探讨相应的解决方案,同时补充 Semaphore、CountDownLatch、CyclicBarrier 相关的面试题及答案。
1. 线程安全的基本概念
原子性 (Atomicity)
指一个操作不可被中断,要么全部执行成功,要么全部失败。例如 i++ 不是原子操作,在多线程环境中可能发生竞态条件。
解决方案:
- 使用 synchronized
- 使用 ReentrantLock
- 使用 Atomic 原子类 (AtomicInteger, AtomicLong等)
- CAS(Compare And Swap)算法
可见性 (Visibility)
指当一个线程修改了某个变量的值,其他线程能够立即看到最新的值。JVM 线程调度时可能使用缓存,使得一个线程在修改变量后,其他线程仍然读到旧值。
解决方案:
- 使用 volatile 关键字
- 使用 synchronized
- 使用 Lock 的显式锁
- 使用 JMM(Java 内存模型)中的
happens-before规则
有序性 (Ordering)
JVM 编译器和 CPU 可能会对指令重排优化,导致代码执行顺序与程序员的预期不同。
解决方案:
- 使用 volatile 禁止指令重排
- 使用 synchronized 和 Lock
- 使用
Thread.sleep()或Thread.yield()强制让出 CPU(低效,不推荐)
2. synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 是否为悲观锁 | 是 | 是 |
| 可重入性 | 支持 | 支持 |
| 公平锁 | 不支持 | 支持公平锁(默认非公平) |
| 中断响应 | 不支持 | 支持 lockInterruptibly() |
| 读写锁 | 不支持 | 支持 ReentrantReadWriteLock |
| 性能 | JDK 1.6 以后优化较好 | 适用于高并发场景 |
面试题:如何选择 synchronized 与 ReentrantLock?
参考答案:一般情况下,synchronized足够应对锁竞争不激烈的场景,且代码简洁,JVM 可以优化。而ReentrantLock提供更丰富的 API,如公平锁、条件变量,适用于高并发和复杂的同步需求。
3. volatile 保证可见性,不保证原子性
示例代码:
class VolatileTest {
private static volatile int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++; // 非原子操作
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
}
}
问题:
volatile仅保证可见性,不保证count++这一操作的原子性。- 结果可能会小于 20000,因为多个线程修改
count可能导致丢失更新。
正确做法:
- 使用
synchronized确保count++的原子性。 - 使用
AtomicInteger代替int类型:
private static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
面试题:volatile 如何保证可见性,又为何不能保证原子性?参考答案:
volatile通过 内存屏障 (Memory Barrier) 确保修改后立即写入主存,同时其他线程会直接读取最新值。- 但
volatile不能解决 竞态条件,因为count++涉及 读-改-写,需要互斥同步(如使用 synchronized 或 AtomicInteger)。
4. CAS (Compare And Swap)
CAS 介绍:
- 比较当前内存值(V)是否为期望值(E),如果是,则修改为新值(N);否则继续重试。
- CAS + 自旋 能够高效处理并发操作,应用于
AtomicInteger等原子类。
示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
System.out.println(count.get());
}
}
CAS 缺点:
- ABA 问题:解决方案使用
AtomicStampedReference - 循环时间过长:高并发环境下可能导致 CPU 资源浪费
- 只能保证一个变量的原子性:多变量情况可使用
AtomicReference
面试题:CAS 有什么缺点?如何优化?
参考答案:① 可能出现 ABA 问题,② 自旋可能无效消耗 CPU,③ 仅适用于单变量。可以通过AtomicStampedReference解决 ABA 问题。
5. AQS(AbstractQueuedSynchronizer)
AQS是 构建锁和同步器的底层框架,底层采用 CLH(双向队列)管理等待线程。- 基于
state状态量实现互斥锁和共享锁 - 被 ReentrantLock、Semaphore、CountDownLatch 层层封装
面试题:AQS 是什么?
参考答案:AQS 是 并发工具类的基础框架,支持state变量控制资源获取,并基于 CLH 队列 管理线程等待队列。
6. Semaphore, CountDownLatch, CyclicBarrier 面试题
(1) Semaphore(信号量)
用于限流/限量访问,通常用于数据库连接池、线程池。
示例代码:
Semaphore semaphore = new Semaphore(3);
semaphore.acquire(); // 获取许可证
// 执行业务逻辑
semaphore.release(); // 释放许可证
面试题:Semaphore 有哪些应用场景?
参考答案:用于 限流(如数据库连接池)或 资源分配控制(如控制 3 个线程访问某个资源)。
(2) CountDownLatch(倒计时器)
等待多个线程完成任务后才继续执行(类似“闭锁”)。
示例代码:
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> { latch.countDown(); }).start();
latch.await(); // 主线程在此等待
面试题:CountDownLatch 如何实现?
参考答案:使用 AQS 实现,倒计次数减少至 0 时,其他等待的线程才能继续执行。
(3) CyclicBarrier(循环栅栏)
所有线程达到屏障后才继续向下执行,可重复使用。
示例代码:
CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // 线程到达屏障等待
面试题:CountDownLatch 和 CyclicBarrier 有什么区别?
参考答案:
- CountDownLatch 只能使用一次;CyclicBarrier 可以重复使用。
- CountDownLatch 侧重于“倒计时”;CyclicBarrier 更适用于 多个线程的同步等待。
线程池数量如何设置
线程池数量的设置对应用的性能和稳定性至关重要。如果线程数量设置过少,会导致 CPU 或其他资源未被充分利用;设置过多,则可能引起线程上下文切换开销过大,反而降低系统性能。因此,合理的线程池数量应根据具体的应用场景进行调整。
1. 线程池的核心参数
在 Java 线程池(ThreadPoolExecutor)中,主要有以下几个关键参数:
- corePoolSize(核心线程数):线程池中始终保持存活的线程数量。
- maximumPoolSize(最大线程数):线程池能够容纳的最大线程数量。
- workQueue(阻塞队列):用于存放等待执行的任务。
核心指标通常围绕 CPU 与 I/O 任务类型来决定“合理的线程数”。
2. 计算线程池的大小
线程池的大小取决于任务的性质,主要可以分为 CPU 密集型 和 I/O 密集型 两种情况。
(1)CPU 密集型任务
CPU 密集型任务主要是指计算类任务,如加解密、科学计算、图像渲染等。这种任务的特点是 CPU 计算占用率很高,线程应该尽量少,以确保 CPU 负载保持在最佳状态。
计算方式
线程池大小 ≈ CPU 核心数 + 1
理由:
- 线程数量等于 CPU 核心数可以保证 CPU 被充分利用。
+1是为了应对线程偶尔阻塞的情况(例如 GC 暂停或线程调度)。
示例
如果服务器是 8 核 CPU:
int coreNum = Runtime.getRuntime().availableProcessors();
int threadPoolSize = coreNum + 1;
通常,核心线程数(corePoolSize)和最大线程数(maximumPoolSize)可以设为相同的值。
(2)I/O 密集型任务
I/O 密集型任务通常涉及网络请求、文件读写、数据库查询等。这类任务的 CPU 计算时间较短,大量时间花费在等待外部资源响应上,因此可以增加线程数来提高吞吐量。
计算方式
线程池大小 ≈ CPU 核心数 × (1 + 平均等待时间 / 计算时间)
该公式也可以简化为:
线程池大小 ≈ CPU 核心数 × 2 或更多
因为 I/O 操作会导致线程频繁处于等待状态,因此可以开更多线程,提高并发能力。
示例
假设服务器是 8 核 CPU:
int coreNum = Runtime.getRuntime().availableProcessors();
int threadPoolSize = coreNum * 2;
3. 其他影响因素
除了任务类型,线程池大小还受到以下因素影响:
- 系统内存大小:线程数过多会占用大量堆空间,可能导致
OutOfMemoryError。 - 数据库连接数:如果线程池任务涉及数据库查询,不应超出数据库连接池的最大线程数。
- 外部 API 限制:如果任务涉及外部接口调用,应考虑请求限流,以防止超出外部 API 允许的访问频率。
- 队列大小:如果
workQueue配置得过大,可能会导致任务堆积,而不会创建新的线程;配置得过小可能会导致任务拒绝或频繁创建销毁线程。
5. 总结
- CPU 密集型任务:线程池大小 ≈
CPU 核心数 + 1 - I/O 密集型任务:线程池大小 ≈
CPU 核心数 × 2(甚至更多) - 同时存在 CPU 和 I/O 任务:可以划分不同的线程池进行管理,分别针对计算任务和 I/O 任务优化。
通过合理地设置线程池大小,可以提高系统吞吐量、减少线程上下文切换开销,最终提升应用的整体性能。💡
Java 中的==和 equals 区别
在 Java 中,== 和 equals() 的主要区别如下:
1. ==(比较地址)
- 基本数据类型:比较的是值是否相等。
- 引用数据类型:比较的是内存地址是否相同(即是否是同一个对象)。
2. equals()(比较内容)
- 默认情况(Object 类):等同于
==,比较地址。 - 某些类(String, Integer 等)重写了
equals(),可以比较内容。 - 自定义类:如需比较内容,需手动重写
equals()。
3. 总结
| 对比项 | == |
equals() |
|---|---|---|
| 作用 | 比较内存地址 | 默认比较地址,但可重写比较内容 |
| 适用 | 基本类型 & 引用类型 | 仅适用于引用类型 |
String |
s1 == s2 比较地址 |
s1.equals(s2) 比较内容 |
Integer |
小范围(-128 ~ 127)相同,大值不同 | 比较数值 |
| 自定义类 | 默认比较地址 | 需重写 equals() 才能比较内容 |
ThreadLocal 介绍及使用场景
1. 什么是 ThreadLocal?
ThreadLocal 是 Java 提供的一种 线程本地存储(Thread-Local Storage, TLS)机制,用于为每个线程提供一个独立的变量副本,避免线程间共享变量时的并发问题。
- 每个线程访问
ThreadLocal变量时,都会创建一个本线程独有的副本,其他线程无法访问。 - 适用于在多线程环境下避免使用全局变量引起的线程安全问题。
2. ThreadLocal 原理
Java 内部使用 ThreadLocalMap(Thread 内部的 Map)存储数据:
- Key:
ThreadLocal引用 - Value:当前线程的变量值副本
每个 Thread 内部都会维护一个 ThreadLocalMap,保证每个线程只访问自己的变量副本,而不会影响其他线程。
public class Thread {
// 每个 Thread 持有一个 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
示意图:
Thread-1 --> ThreadLocal --> [变量A: 100]
Thread-2 --> ThreadLocal --> [变量A: 200]
每个线程访问 ThreadLocal 变量时,获取的都是自己独有的副本,互不干扰。
4. ThreadLocal 常见使用场景
📌(1)每个线程存储自己的变量
用于存储线程单独的变量副本,避免线程共享数据导致的同步问题。
private static ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
// 每个线程都有自己的 counter,不会相互影响
📌(2)数据库连接(事务管理)
在 Spring 事务管理中,ThreadLocal 可用于让每个线程存储自己的 数据库连接对象(如 Connection),确保同一线程使用同一个连接,避免多个线程混用同一数据库连接。
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
if (connectionHolder.get() == null) {
// 创建新的数据库连接,并绑定到当前线程
connectionHolder.set(createNewConnection());
}
return connectionHolder.get();
}
}
📌(3)用户身份信息(请求上下文存储)
在 Web 应用中,使用 ThreadLocal 存储当前用户 ID,避免在方法参数中传递用户信息。
public class UserContext {
private static ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public static void setUser(String userId) {
userThreadLocal.set(userId);
}
public static String getUser() {
return userThreadLocal.get();
}
}
然后在 Web 请求进入时,设置用户信息:
// 设置当前线程的用户信息
UserContext.setUser("user123");
// 在业务方法中随时获取当前用户
System.out.println(UserContext.getUser());
📌(4)ThreadLocal+SimpleDateFormat 避免线程问题
SimpleDateFormat 不是线程安全的,直接共享时会产生问题。可以使用 ThreadLocal 让每个线程拥有单独的 SimpleDateFormat 对象:
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String formatDate(Date date) {
return dateFormatThreadLocal.get().format(date);
}
5. ThreadLocal 内存泄露问题
💡 问题
ThreadLocal 变量存放在 ThreadLocalMap 中,Key 是弱引用,但 Value 是强引用,当 ThreadLocal 变量被回收后,Thread 仍然持有 Value,可能会导致内存泄漏。
🚨 解决方案
每次使用 ThreadLocal 后,手动调用 remove() 方法,确保内存释放:
try {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(100);
System.out.println(threadLocal.get());
} finally {
threadLocal.remove(); // 清理 ThreadLocal 变量,防止内存泄漏
}
6. ThreadLocal vs 线程安全机制
| 机制 | ThreadLocal | synchronized / Lock |
|---|---|---|
| 目的 | 避免线程共享数据 | 保证线程共享数据的同步 |
| 方式 | 每个线程独有自己副本 | 通过加锁控制多个线程访问 |
| 适用场景 | 每个线程需要独立变量副本(如数据库连接、用户信息) | 多个线程需要共享同一数据 |
7. 总结
| ThreadLocal 作用 | 解释 |
|---|---|
| 线程独享变量副本 | 每个线程单独存储数据,互不影响 |
| 避免同步问题 | 代替 synchronized 解决线程安全问题 |
| 常见应用场景 | 事务管理(数据库连接)、用户会话、SimpleDateFormat |
| 需注意的点 | ThreadLocal 变量要 remove() 以防内存泄漏 |
📌 选用建议
✅ 适用于:
- 每个线程都需要自己的数据副本, 避免线程同步问题(如用户 ID 传递、数据库连接)。
- 高并发情况下,避免
synchronized造成的性能开销。
❌ 不适用于:
- 多个线程需要共享数据(应使用
synchronized或Atomic变量)。 - 长期使用,不手动
remove()会导致内存泄漏。
ReentrantLock 的实现原理
ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 提供的一种可重入、可中断的显示锁,相比 synchronized 具有更多灵活的同步控制机制,如公平锁、非公平锁等。
1. ReentrantLock 的核心特点
✅ 可重入锁(Reentrant):同一线程可以多次获取同一把锁,并累计锁的次数。
✅ 可选公平锁/非公平锁:可以选择公平锁(先来先得)或非公平锁(默认,更高效)。
✅ 支持中断:线程等待锁时,可由 interrupt() 打断,避免死锁问题。
✅ 支持 tryLock():可以尝试获取锁,并设置超时时间。
✅ 支持 Condition 机制:可以实现 wait/notify 类似的等待通知操作(比 synchronized 更强大)。
2. ReentrantLock 的底层实现
(1)AQS(AbstractQueuedSynchronizer)
ReentrantLock 的底层原理依赖 AQS(AbstractQueuedSynchronizer),它提供了FIFO 队列来管理线程的排队获取锁的逻辑。
-
锁的状态(state):在
AQS中,使用state变量表示锁的状态:- state = 0:锁是空闲状态。
- state > 0:锁已被某个线程占用,且值代表重入次数。
-
加锁/解锁逻辑
- 当一个线程想获取锁时,如果
state == 0,则成功获取并设置state = 1。 - 如果该线程再次获取锁(重入),
state递增。 - 释放锁时,
state--,直到state = 0表示锁完全释放。
- 当一个线程想获取锁时,如果
3. ReentrantLock 获取锁的过程
ReentrantLock 提供两种锁:
- 非公平锁(默认)——直接抢占锁,提高吞吐量(但可能导致饥饿);
- 公平锁——按请求顺序分配锁(但性能略低)。
// 创建一个非公平锁(默认)
ReentrantLock lock = new ReentrantLock();
// 创建一个公平锁
ReentrantLock fairLock = new ReentrantLock(true);
(1)非公平锁获取锁(默认方式)
调用 lock() 方法时:
- 直接尝试使用
CAS (compareAndSet)修改 state 为1(表示加锁成功)。 - 如果失败,则进入 AQS 队列等待。
public void lock() {
sync.lock();
}
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) { // 尝试CAS加锁
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1); // 进入AQS等待队列
}
}
}
(2)公平锁获取锁
先检查队列是否为空,如果队列前面有等待中的线程,则遵循 FIFO 规则排队。
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 如果 state == 0,并且前面**没有线程等待**,则成功加锁
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
return false;
}
- 当
state == 0且 前面没有线程排队时,才会成功获取锁。 - 否则,加入等待队列,等待唤醒。
4. ReentrantLock 释放锁的过程
当线程执行 unlock() 时,会减少 state 计数:
state--直到state = 0时,彻底释放锁并唤醒下一个等待线程。
public void unlock() {
sync.release(1);
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (c == 0) {
setExclusiveOwnerThread(null); // 释放锁的线程
setState(0); // 释放 state
return true;
}
setState(c); // 递减 state
return false;
}
5. ReentrantLock vs synchronized
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现方式 | 基于 AQS | JVM 内置 |
| 可重入性 | ✅ | ✅ |
| 公平性 | 支持公平 & 非公平锁 | 只能是非公平锁 |
| 中断获取 | ✅lockInterruptibly() |
❌ |
| 尝试获取 | ✅tryLock() |
❌ |
| 等待通知机制 | Condition(更强大) |
wait/notify |
| 性能 | 高并发情况下更优 | 低锁竞争时更快 |
▶ 推荐使用场景
- 低竞争:
synchronized更适合,JVM 会优化。 - 高竞争:
ReentrantLock更高效,支持tryLock()&Condition。
6. 典型使用示例
(1)基本加锁/解锁
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> accessResource(), "Thread-1").start();
new Thread(() -> accessResource(), "Thread-2").start();
}
private static void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁...");
}
}
}
(2)支持 tryLock()
尝试获取锁,防止死锁:
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("未获取锁,避免死锁!");
}
(3)支持 lockInterruptibly()
public void task() {
try {
lock.lockInterruptibly();
System.out.println("执行任务...");
} catch (InterruptedException e) {
System.out.println("线程被中断");
} finally {
lock.unlock();
}
}
7. 总结
| 特性 | ReentrantLock 作用 |
|---|---|
| 底层原理 | 基于 AQS 的 state 变量控制锁 |
| 默认锁类型 | 非公平(可以选择公平锁) |
| 可重入性 | 支持重入,同一线程可多次获取锁 |
| 获取锁方式 | lock() / tryLock() / tryLock(超时) |
| 打断等待 | lockInterruptibly() 支持中断 |
| Condition 机制 | newCondition() 代替 wait/notify |
JVM 运行时数据区有哪些?它们的作用是什么?
JVM 运行时数据区主要包括:
-
程序计数器(PC Register):
- 线程私有,记录当前 JVM 线程执行的字节码指令地址。
- 线程执行 JNI 方法时,PC 计数器值为空(Undefined)。
-
Java 虚拟机栈:
- 线程私有,用于管理 Java 方法调用的栈帧,每个方法执行时都会创建新的栈帧。
- 包含 局部变量表(存放方法中的基本数据类型、对象引用)、操作数栈(存放方法执行过程中的操作数)、动态链接、方法返回地址等。
- 可能发生
StackOverflowError(栈空间不足)。
-
本地方法栈(Native Method Stack):
- 线程私有,与 JVM 栈类似,但它服务于本地方法(Native 方法)。
- 可能触发
StackOverflowError。
-
堆(Heap):
- 线程共享,存放所有的 Java 对象及数组,是垃圾回收的主要区域。
- JDK 1.8 之后,永久代(PermGen)被元空间(Metaspace)替代,并由直接内存管理。
-
方法区(Method Area):
- 线程共享,存储 类元信息、方法代码、常量池、静态变量 等。
- JDK 8 之后,原来的永久代被元空间取代,减少 OOM 发生。
-
运行时常量池(Runtime Constant Pool):
- 属于 方法区,存放编译期生成的符号引用、字符串常量等。
JVM 的垃圾回收机制?常见的 GC 算法有哪些?
JVM 自动内存管理的核心是垃圾回收(GC,Garbage Collection),GC 主要用于回收不再使用的对象。常见的 GC 算法如下:
-
标记-清除(Mark-Sweep):
- 步骤:
- 标记所有存活对象。
- 清除不可达对象。
- 优点:简单,适用于老年代。
- 缺点:碎片化问题。
- 步骤:
-
复制算法(Copying):
- 步骤:
- 将存活对象从 From 空间复制到 To 空间,清除 From 空间。
- 优点:解决碎片化问题,适用于新生代。
- 缺点:浪费 50% 空间。
- 步骤:
-
标记-整理(Mark-Compact):
- 步骤:
- 标记所有存活对象。
- 整理存活对象 到一端。
- 清除无用对象。
- 适用场景:老年代减少碎片化。
- 步骤:
什么是 JVM 的分代回收机制?为什么要分代?
JVM 内存管理采用分代回收机制(Generational GC),主要目的是提高垃圾回收性能。
-
新生代(Young Generation)
- 适用于存活时间短的对象。
- Eden + 两个 Survivor(S0、S1)区域。
- 回收时采用复制算法,效率高。
Minor GC专门回收新生代,对象通常发生多次 GC 就被收集。
-
老年代(Old Generation)
- 存活较长的对象(经过多轮 GC from 新生代晋升)。
- 标记-清除/标记-整理算法用于老年代回收。
Full GC针对整个堆。
-
永久代(已废弃)/ 元空间(Metaspace)
- 永久代(JDK 1.7 及以前)存储类信息、方法元数据。
- JDK 8 以后采用元空间(Metaspace),避免
OutOfMemoryError。
类加载的生命周期是什么?
Java 的 类加载过程 包括:
- 加载(Loading):从 Class 文件加载字节码数据到内存。
- 验证(Verification):确保字节码数据安全合法。
- 准备(Preparation):给静态变量分配初始值(不是赋值)。
- 解析(Resolution):将符号引用转换为直接引用。
- 初始化(Initialization):执行静态代码块、赋静态变量值。
JVM 类加载器有哪些?
Java 使用 双亲委派模型(Parent Delegation Model) 进行类加载,主要的加载器包括:
-
启动类加载器(Bootstrap ClassLoader)
- 加载 Java 标准库(rt.jar)。
-
扩展类加载器(ExtClassLoader)
- 加载
lib/ext/目录或java.ext.dirs变量指定的路径中的类。
- 加载
-
应用类加载器(AppClassLoader)
- 默认加载
classpath下的类。
- 默认加载
-
自定义类加载器:
- 继承
ClassLoader,可控类加载规则。
- 继承
双亲委派模型原理:
- 当类加载器收到类加载请求,它先请求父加载器加载,父加载器无法加载时,才由自身加载。
如何排查 JVM OOM(内存溢出)问题?
OOM(OutOfMemoryError)表示 JVM 无法再分配足够的内存 来满足应用程序的内存需求。它通常发生在堆、永久代(Metaspace)、栈、直接内存等区域。
1. Java 堆溢出(java.lang.OutOfMemoryError: Java heap space)
- 创建了过多的对象,无法被 GC 及时回收,导致堆空间不足。
- 内存泄漏,例如:对象被 静态变量引用 或 未适当地从集合中移除。
示例
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) { // 无限添加对象,占满堆
list.add(new OOMObject());
}
}
}
如何解决?
- 分析 GC 日志:开启
-XX:+PrintGCDetails检查是否有频繁 GC 或者对象未被回收。 - 增大堆大小,调整
-Xmx,例如-Xmx2G增加堆内存。 - 优化代码、减少对象创建:
- 使用 对象池(线程池、数据库连接池)
- 及时释放无用对象 (
WeakReference),或使用SoftReference - 使用 堆分析工具(如
jmap、MAT)
2. 元空间溢出(java.lang.OutOfMemoryError: Metaspace)
原因
- 类加载过多(如频繁 动态生成类)。
- Spring 动态代理导致类元信息无法及时回收。
示例
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class MetaspaceOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOOM.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 无限创建动态代理类
}
}
}
如何解决?
- 增大 Metaspace:
-XX:MaxMetaspaceSize=256M - 通过
jstat -gcutil <pid>检查类加载情况,避免动态类加载过多。 - 减少动态代理/反射使用,改用
Java Proxy代替CGLIB。
3. 线程栈溢出(java.lang.OutOfMemoryError: unable to create new native thread)
原因
- 创建过多线程,超出系统
max user processes限制(Linuxulimit -u)。 - 单个线程栈过大,导致
-Xss配置超出系统限制。
示例
public class StackOOM {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
while (true) {}
}).start(); // 无限创建线程
}
}
}
如何解决?
- 限制线程创建数量(用线程池
ExecutorService控制)。 - 减小单线程占用内存(调整
-Xss1024k)。 - 降低 CPU 限制(Linux
ulimit -u限制用户可创建的线程数)。 - 使用
jstack分析Thread Dump,找到过多线程的来源。
4. 直接内存溢出(java.lang.OutOfMemoryError: Direct buffer memory)
原因
- 大量使用 NIO(Netty、DirectByteBuffer),导致直接内存(
-XX:MaxDirectMemorySize)耗尽。 - 直接操作 ByteBuffer 不释放(例如
Netty没有release())。
示例
import java.nio.ByteBuffer;
public class DirectMemoryOOM {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
ByteBuffer.allocateDirect(512 * 1024 * 1024); // 分配 512MB 直接内存
}
}
}
如何解决?
- 限制
-XX:MaxDirectMemorySize=512M - 使用 堆内存
heap ByteBuffer(allocate()) 代替DirectByteBuffer。 - 及时手动释放 ByteBuffer:
((DirectBuffer)buffer).cleaner().clean();
5. GC 超时(java.lang.OutOfMemoryError: GC overhead limit exceeded)
原因
- GC 过于频繁,CPU 大部分时间都在垃圾回收,但仍无法释放足够空间。
- 对象存活时间过长,堆空间不足。
示例
import java.util.HashMap;
public class GCOverheadOOM {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
int i = 0;
while (true) {
map.put(i++, "Test String");
}
}
}
如何解决?
- 增大
-Xmx - 调整 GC 策略,如 G1 GC
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 - 使用
VisualVM或MAT查看对象存活情况,优化代码。
如何排查 OOM 问题?
1. 打印 GC 日志
-XX:+PrintGCDetails -Xloggc:gc.log
分析 GC 频率、回收时间、堆使用情况。
2. 生成 Heap Dump
jmap -dump:format=b,file=heap_dump.hprof <pid>
然后使用 MAT(Memory Analyzer Tool) 分析 对象占用情况。
3. 线程快照(Thread Dump)
jstack <pid>
查看 死锁、线程阻塞情况。
4. 使用 jcmd 分析
jcmd <pid> VM.flags
查看 JVM 配置参数,检查 -Xmx 是否足够。
总结
| OOM 类型 | 原因 | 解决方案 |
|---|---|---|
| 堆溢出 | 对象过多 or 内存泄漏 | 增大堆、优化代码、Memory Dump 分析 |
| 元空间溢出 | 类加载过多 or 动态代理 | 限制动态类加载,减少 CGLIB 代理 |
| 栈溢出 | 线程过多 or 栈空间不足 | 限制线程数,优化 -Xss |
| 直接内存溢出 | NIO ByteBuffer 分配过大 |
限制 MaxDirectMemorySize,手动释放内存 |
| GC 频繁后 OOM | 存活对象过多,没有足够空间 | 调整 GC 策略,增大堆大小 |
通过 GC 日志、Heap Dump、线程分析 等工具,可以快速发现 OOM 问题的根因,并采取适当的优化措施 🚀!
JVM 如何调优?性能优化策略有哪些?
JVM 性能优化主要分为:
-
调整堆大小
-Xms(初始堆大小)、-Xmx(最大堆大小)
-
调整新生代大小
-Xmn控制新生代大小,保证 Minor GC 频率适中。
-
垃圾回收器选择
-XX:+UseG1GC(最新 Server 级垃圾回收器)。-XX:+UseParallelGC(高吞吐)。-XX:+UseZGC(低延迟)。
-
GC 日志调优
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
JMM(Java Memory Model) 高级面试题及解析
1. 什么是 Java 内存模型(JMM)?
Java 内存模型(Java Memory Model,JMM)是 Java 语言的并发机制的一部分,定义了 Java 线程如何可见、有序和同步地访问共享变量,主要解决 CPU 指令重排、线程缓存可见性、并发数据一致性 等问题。
JMM 主要保证的三个特性
- 可见性(Visibility):一个线程对共享变量的修改,能被其他线程立即看到(例如
volatile变量)。 - 有序性(Ordering):保证代码执行顺序按照 happens-before 规则,而不会被 CPU 乱序执行破坏一致性。
- 原子性(Atomicity):确保一个操作不可被中断(如
synchronized或AtomicInteger)。
2. JMM 为什么需要 volatile 关键字?
volatile 主要解决 变量可见性 和 指令重排 问题。
- 保证可见性:
- 当一个线程修改
volatile变量后,其他线程立即可见,不会读取“脏数据”。
- 当一个线程修改
- 禁止指令重排:
volatile变量的 写操作 不能与前面的读操作 重排序。
- 不保证原子性:
volatile++仍然是非原子的(需要AtomicInteger或synchronized)。
class VolatileExample {
private volatile boolean flag = true;
public void stop() {
flag = false; // 线程1修改变量
}
public void run() {
while (flag) { // 线程2能立即看到 flag 变 false
}
}
}
如果不加 volatile,线程可能一直看不到** flag=false,造成死循环!
3. synchronized 和 volatile 的区别
| 特性 | volatile |
synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 指令重排 | 防止 | 防止 |
| 适用场景 | 适用于简单变量 | 适用于多个操作的代码块 |
4. volatile 变量一定是线程安全的吗?
volatile 仅保证可见性,不保证原子性,所以 不能保证线程安全。
class Counter {
private volatile int count = 0;
public void increment() {
count++; // 非原子性操作 -> 线程安全问题
}
}
⚠ 可能问题
- 线程 A 读取
count = 0 - 线程 B 也读取
count = 0 - 线程 A 和 B 分别写入
count = 1(丢失更新!)
如何修复?
✔ 使用 AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 线程安全
}
5. 什么是 happens-before 规则?
happens-before 规则确保某个操作的结果对其他线程可见。
几种常见 happens-before 规则
- 单线程顺序性:单个线程内代码执行顺序与代码顺序一致。
volatile规则:写 happens-before 读,保证最新的值可见。synchronized规则:一个同步块的解锁(unlock) 必然发生在 同一个锁的 加锁(lock) 之前。Thread.start()规则:Thread.start()之前的所有代码,必然发生在子线程的run()之后。
volatile int x = 0, y = 0;
void writer() {
x = 1; // A
y = 2; // B
}
void reader() {
int a = x; // C
int b = y; // D
}
根据 happens-before 规则:
A happens-before B,但C可能看到y=2但x=0(乱序执行)。volatile只能保证 读写顺序,但无法保证多个变量的相对顺序。
✔ 解决方案:
- 使用
synchronized或Lock确保代码块的整体原子性。
6. synchronized 的底层实现?
- 实现方式:
synchronized关键字可用于 方法 和 代码块,底层依赖于 JVMMonitor(监视器) 实现。
-
优化机制:
- 偏向锁(Biased Locking):如果只有一个线程使用锁,提高性能。
- 轻量级锁(Lightweight Locking):多个线程竞争前不会进入
Monitor(CAS 自旋)。 - 重量级锁(Heavyweight Locking):线程竞争激烈时,进入
Monitor,涉及 OS 线程阻塞(Park)。
7. synchronized 和 Lock 的区别?
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 实现方式 | JVM 级别 | JDK 层面 |
| 是否可中断 | 否 | 是(lockInterruptibly) |
| 读写锁 | 不支持 | ReadWriteLock 支持 |
| 条件变量 | 不支持 | Condition 支持 |
✔ Lock 更灵活,但 synchronized 更简单易用,并且在 JDK 1.6 之后优化了性能,如 偏向锁、轻量级锁。
8. AtomicInteger 底层实现
AtomicInteger 依赖 CAS(Compare-And-Swap) 实现无锁操作。
CAS 概念
- 线程获取变量值
V,检查是否仍是期望值E,如果是,就更新为N值,否则重试。
CAS 代码示例
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet(); // 内部使用 CAS
}
✔ 底层使用 Unsafe.compareAndSwapInt() 进行无锁更新,提高并发性能。
9. 为什么 volatile 不能代替 synchronized?
- volatile 不能保证原子性:
volatile++由于 读取-修改-写入 是非原子操作,不能防止竞态条件(race condition)。
- volatile 不能实现互斥:
synchronized或Lock可以保障代码块的互斥访问,volatile只能保证共享变量最新可见,但不能保证多个操作的完整性。
✔ 正确实践:
如果变量是单次读写(如配置值),用 volatile;如果变量涉及多个操作(如计算器加减),用 synchronized 或 Lock。
结论
- JMM 的本质是确保线程安全和最大化并发性能。
volatile适合 “可见性” 场景但不保证原子性。synchronized适合 同步代码块,并且 JDK 1.6 之后性能优化 不逊于Lock。AtomicInteger采用 CAS(Compare-And-Swap) 实现高效的 无锁更新。
📌 Spring Web 核心原理详解
Spring Web 及其原理是面试中的重要考察点,以下内容将详细阐述 Spring MVC、Spring Boot 自动装配、Spring 事务、Spring AOP、Bean 生命周期
📍 1. Spring MVC 运行流程及底层原理
Spring MVC 是如何工作的?它实现了什么设计模式?
Spring MVC 是 基于前端控制器(DispatcherServlet)模式,核心基于 委派模式、策略模式、适配器模式 等来实现请求分发。
🔹 Spring MVC 工作流程
- 用户请求 -> DispatcherServlet
DispatcherServlet作为前端控制器拦截所有请求- 入口方法:
doDispatch()
- 通过 HandlerMapping 解析请求
HandlerMapping解析请求 URL,并映射到 Controller 方法,比如@RequestMapping("/user")
- HandlerAdapter 执行 Controller 方法
- HandlerAdapter 适配请求转发至
Controller方法, 并使用反射调用@Controller方法
- HandlerAdapter 适配请求转发至
- Controller 处理请求,返回 ModelAndView
ModelAndView封装数据(Model)和视图名(View)
- ViewResolver 解析视图
ViewResolver解析视图,并渲染 HTML (Thymeleaf / JSP 等)
- 响应返回给用户
DispatcherServlet最终返回 HTTP 响应
🔹 设计模式分析
| 设计模式 | 在 Spring MVC 中的应用 |
|---|---|
| 前端控制器模式 | DispatcherServlet 控制所有请求 |
| 策略模式 | 通过 HandlerAdapter 适配不同的 Controller 处理器 |
| 适配器模式 | HandlerAdapter 适配 Controller,支持 @Controller、@RestController 等 |
📌 底层源码解析
Spring MVC 的 DispatcherServlet#doDispatch() 是核心调度方法:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
processDispatchResult(request, response, mappedHandler, mv);
}
核心机制
getHandler(request): 通过 HandlerMapping 查找 Controller 方法。getHandlerAdapter(handler): 获取合适的HandlerAdapter适配器执行 Controller。ha.handle(request, response, handler): 反射调用Controller方法。processDispatchResult(): 处理返回结果,交给 ViewResolver 渲染视图。
📍 2. Spring Boot 自动装配原理
Spring Boot 是如何实现自动配置的?
Spring Boot 通过 EnableAutoConfiguration + SpringFactoriesLoader 机制 实现自动装配。
🔹 关键机制
🔹 Spring Boot 启动时:
@SpringBootApplication启动主方法@EnableAutoConfiguration触发AutoConfigurationImportSelector- SpringFactoriesLoader 加载
META-INF/spring.factories - 查找
@Configuration类,并创建 Bean
📌 代码分析
Spring Boot @SpringBootApplication:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
}
EnableAutoConfiguration 核心代码:
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
AutoConfigurationImportSelector 负责自动加载 spring.factories:
@Override
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
return SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), classLoader);
}
总结
SpringFactoriesLoader机制解析META-INF/spring.factories- 以
@Configuration形式加载自动配置类,如DataSourceAutoConfiguration
📍 3. Spring 事务管理原理
Spring 事务 @Transactional 是如何工作的?它的底层原理是什么?
Spring 事务管理基于 AOP 代理 实现,核心原理包括:
- 事务管理器
TransactionManager负责事务事务开启 / 提交 / 回滚 - 基于动态代理(JDK / CGLIB)拦截
@Transactional方法 - 代理对象调用方法前,开启事务,调用目标方法
- 方法执行成功提交事务,异常时回滚事务
底层代理方式
- JDK 动态代理:适用于 实现接口的 Service
- CGLIB 代理:适用于 无接口类
📌 代码分析
Spring 通过 TransactionAspectSupport 进行 AOP 事务代理:
public final class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
}
- 拦截
@Transactional方法 - 调用
invokeWithinTransaction()处理事务 - commit 或 rollback 事务
📍 4. Spring AOP 原理 & 动态代理原理
Spring AOP 是如何实现的?动态代理如何工作?
Spring AOP 基于动态代理(JDK 代理 / CGLIB 代理) 实现切面增强:
- JDK 代理:基于
Proxy.newProxyInstance(),适用于 接口代理 - CGLIB 代理:基于 字节码增强(ASM),适用于 无接口代理
📌 代码分析
1️⃣ JDK 动态代理
public class JdkDynamicProxy implements InvocationHandler {
private Object target;
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强...");
Object result = method.invoke(target, args);
System.out.println("后置增强...");
return result;
}
}
2️⃣ CGLIB 代理
public class CglibProxy implements MethodInterceptor {
public Object getInstance(Object target) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 前置增强...");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 后置增强...");
return result;
}
}
✅ 总结
💡 Spring MVC:基于 前端控制器模式,解析请求并调用 Controller。
💡 Spring Boot 自动装配:基于 spring.factories + SpringFactoriesLoader 实现。
💡 Spring 事务管理:基于 AOP 代理 + 事务管理器 实现事务回滚。
💡 Spring AOP:通过 JDK 动态代理 / CGLIB 代理 实现切面增强。
🎯 微服务架构-服务治理
在微服务架构中,Spring Boot 是常用的基础框架,而Spring Cloud 提供了一整套的微服务治理方案。本文将详细解析 Spring Boot 在微服务中的服务治理方案,包括 注册与发现、负载均衡、网关、熔断限流、服务监控 等。
📌 1. 什么是微服务治理?
✅ 面试解答
💡 微服务架构涉及以下关键问题:
- 服务注册与发现(Service Discovery)
- 负载均衡(Load Balancing)
- API 网关(API Gateway)
- 服务熔断 & 限流
- 服务监控 & 链路追踪
⚡ 常见微服务治理框架
📍 Spring Cloud Netflix(Eureka、Ribbon、Hystrix)
📍 Spring Cloud Alibaba(Nacos、Sentinel)
📍 API Gateway(Spring Cloud Gateway、Zuul)
📍 分布式链路追踪(Zipkin、Sleuth)
📌 2. Spring Boot 如何进行服务注册与发现?
Spring Boot 使用 Eureka 或 Nacos 进行服务注册与发现。
(1)基于 Eureka 进行服务注册📖
💡 Eureka 是 Netflix 提供的注册中心,Spring Cloud 提供了客户端支持。
📌 1️⃣ 启动 Eureka Server
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
📌 2️⃣ 注册微服务
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 连接到 Eureka Server
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
📌 3️⃣ 查询注册的微服务
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/services")
public List<String> serviceList() {
return discoveryClient.getServices();
}
(2)使用 Nacos 进行服务注册📖
📌 1️⃣ 添加 Nacos 依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
📌 2️⃣ 服务配置 application.yml
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 连接到 Nacos Server
📌 3️⃣ 启动 Nacos 客户端
@EnableDiscoveryClient
@SpringBootApplication
public class NacosServiceApplication {
public static void main(String[] args) {
SpringApplication.run(NacosServiceApplication.class, args);
}
}
📌 4️⃣ 调用另一个微服务
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/call-service")
public String callService() {
return restTemplate.getForObject("http://order-service/orders", String.class);
}
📌 3. Spring Boot 如何实现负载均衡?
✅ 面试问题
如何在 Spring Boot 微服务中实现负载均衡?
📌 解答
📍 Spring Cloud 提供 Ribbon(客户端负载均衡) 和 Nacos 负载均衡
📍 也可搭配 Spring Cloud Gateway / Zuul 进行路由级负载均衡
📌 🔹 基于 Ribbon
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
restTemplate.getForObject("http://order-service/orders", String.class);
📌 🔹 基于 Feign
@FeignClient("order-service")
public interface OrderClient {
@GetMapping("/orders")
String getOrders();
}
@Autowired
private OrderClient orderClient;
@GetMapping("/orders")
public String fetchOrders() {
return orderClient.getOrders();
}
📌 4. 如何在 Spring Boot 微服务中实现 API 网关?
✅ 面试问题
如何使用 Spring Cloud Gateway 实现微服务网关?
📌 解答
📌 Spring Cloud Gateway 是 Spring 官方网关,代替 Zuul 进行 API 路由
📌 1️⃣ 添加 Spring Cloud Gateway 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
📌 2️⃣ 配置路由
spring:
cloud:
gateway:
routes:
- id: order_service
uri: http://localhost:8001
predicates:
- Path=/orders/**
📌 3️⃣ 自定义全局 Filter
@Component
public class CustomGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("请求通过网关...");
return chain.filter(exchange);
}
}
📌 5. 如何在微服务中实现熔断限流?
✅ 面试问题
在 Spring Boot 微服务架构中,如何防止雪崩效应?
📌 解答
✅ 熔断(Circuit Breaker) & 限流(Rate Limiting)
- Hystrix(停止维护) -> Resilience4j
- Sentinel(阿里巴巴开源,高级流量管控支持)
📌 🔹 Hystrix 熔断
@HystrixCommand(fallbackMethod = "fallbackMethod")
public String getData() {
return restTemplate.getForObject("http://order-service/orders", String.class);
}
public String fallbackMethod() {
return "服务故障,返回降级信息";
}
📌 🔹 Sentinel 限流
spring:
cloud:
sentinel:
enabled: true
@SentinelResource(value = "rateLimit", blockHandler = "blockHandlerMethod")
public String protect() {
return "正常返回";
}
public String blockHandlerMethod(BlockException ex) {
return "请求被限流";
}
📌 6. 服务监控 & 链路追踪
✅ 面试问题
Spring Boot 如何监控微服务?如何进行分布式链路追踪?
📌 🔹 Spring Boot Actuator 进行服务监控
management:
endpoints:
web:
exposure:
include: "*"
访问:
http://localhost:8080/actuator/health
📌 🔹 使用 Zipkin 进行分布式链路追踪
spring:
zipkin:
base-url: http://localhost:9411
🎯 总结
✅ 服务注册发现 👉 Eureka / Nacos
✅ 负载均衡 👉 Ribbon / Feign / Gateway
✅ API 网关 👉 Spring Cloud Gateway
✅ 熔断限流 👉 Hystrix / Sentinel
✅ 监控 & 链路追踪 👉 Spring Boot Actuator + Zipkin
🌟 微服务治理面试题
| 微服务治理 | 面试问题 | 难度 |
|---|---|---|
| 1. 服务注册与发现 | 🌟Spring Boot 如何实现服务注册与发现?Eureka 和 Nacos 的区别? | ⭐⭐⭐ |
| 2. 负载均衡 | 🌟Spring Boot 如何实现负载均衡?Ribbon vs Feign? | ⭐⭐⭐⭐ |
| 3. API 网关 | 🌟Spring Cloud Gateway 是如何工作的?Zuul vs Gateway? | ⭐⭐⭐⭐ |
| 4. 熔断限流 | 🌟Spring Boot 如何防止雪崩效应?Hystrix & Sentinel 机制? | ⭐⭐⭐⭐⭐ |
| 5. 服务监控 & 链路追踪 | 🌟Spring Boot 如何进行服务监控和日志链路追踪? | ⭐⭐⭐ |
📌 1. Spring Boot 如何实现服务注册与发现?(Eureka & Nacos)
💡 面试问题
- 什么是注册中心?为什么需要 Eureka / Nacos?
- Eureka 和 Nacos 的区别?
- 如何在 Spring Cloud 使用 Eureka / Nacos?
✅ 详细解答
🌍 服务注册中心(Service Registry) 作用:
- 维护服务地址,以便其他微服务可以发现它们
- 使服务间调用不依赖固定 IP,而是通过服务名访问
📍 Eureka vs Nacos
| 对比项 | Eureka | Nacos |
|---|---|---|
| 注册方式 | CP(数据一致性) | AP(可用性优先) |
| 健康检查 | 客户端主动上报 | Server 端定期探测 |
| Spring Boot 兼容 | Spring Cloud Netflix | Spring Cloud Alibaba |
📌 代码实现
1️⃣ Eureka Server(注册中心)
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
2️⃣ Eureka Client(服务提供方)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
@EnableDiscoveryClient
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
📌 2. Spring Boot 如何实现负载均衡?Ribbon vs Feign
💡 面试问题
- 为什么微服务需要负载均衡?
- Ribbon 和 Feign 有什么区别?
- Nacos 如何实现负载均衡?
✅ 详细解答
📍 负载均衡原理:
- 让多个微服务实例 均衡分配请求
- 保障高可用 & 扩展性
- 典型负载均衡策略:
- Ribbon(客户端负载均衡)
- Feign(REST 调用 + 负载均衡)
- Nacos(内置负载均衡)
📍 Ribbon vs Feign
| 对比项 | Ribbon | Feign |
|---|---|---|
| 作用 | 纯负载均衡组件(RestTemplate) | 声明式 HTTP 客户端 |
| 是否自动 | 需手动调用 | 内置负载均衡 |
| 整合方式 | RestTemplate |
@FeignClient |
📌 代码实现
1️⃣ Ribbon 负载均衡
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/fetch")
public String fetchOrders() {
return restTemplate.getForObject("http://order-service/orders", String.class);
}
2️⃣ Feign 负载均衡
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/orders")
String getOrders();
}
@Autowired
private OrderClient orderClient;
@GetMapping("/orders")
public String fetchOrders() {
return orderClient.getOrders();
}
📌 3. Spring Cloud Gateway 是如何工作的?
💡 面试问题
- Spring Cloud Gateway 的核心功能是什么?
- Zuul 和 Gateway 的区别?
✅ 详细解答
📍 API 网关作用
- 统一路由管理(动态路由)
- 认证 & 权限控制(JWT)
- 限流、日志 & 监控
- 熔断 & 异常处理
- 负载均衡
📍 Zuul vs Spring Cloud Gateway
| 对比项 | Zuul | Spring Cloud Gateway |
|---|---|---|
| 底层技术 | Servlet | Reactor(Netty) |
| 性能 | 较低 | 高性能 |
| 推荐使用 | 逐步淘汰 | 官方推荐 |
📌 代码实现
spring:
cloud:
gateway:
routes:
- id: order_service_route
uri: lb://order-service
predicates:
- Path=/orders/**
📌 4. Spring Boot 如何防止雪崩效应?
💡 面试问题
- 什么是熔断?什么是限流?
- Hystrix 和 Sentinel 的区别?
- 如何配置 Sentinel 限流?
✅ 详细解答
📍 熔断(Circuit Breaker):防止下游服务崩溃
📍 限流(Rate Limiting):防止流量过载
📍 Hystrix vs Sentinel
| 对比项 | Hystrix(Netflix) | Sentinel(阿里巴巴) |
|---|---|---|
| 熔断策略 | 断路器模式 | 滑动窗口统计 |
| 限流 | 不支持 | 提供多种限流算法 |
| 推荐使用 | 逐步淘汰 | Spring Cloud Alibaba |
📌 代码示例
📌 1️⃣ Hystrix 熔断
@HystrixCommand(fallbackMethod = "fallbackMethod")
public String getData() {
return restTemplate.getForObject("http://order-service/orders", String.class);
}
public String fallbackMethod() {
return "服务降级返回默认值";
}
📌 2️⃣ Sentinel 限流
@SentinelResource(value = "rateLimit", blockHandler = "blockHandlerMethod")
public String protect() {
return "正常请求";
}
public String blockHandlerMethod(BlockException ex) {
return "请求被限流";
}
📌 5. Spring Boot 如何进行监控和链路追踪?
📌 代码示例
📌 1️⃣ Spring Boot Actuator 监控
management:
endpoints:
web:
exposure:
include: "*"
📌 2️⃣ Zipkin 链路追踪
spring:
zipkin:
base-url: http://localhost:9411
🎯 总结
🎯 注册 & 发现 → Eureka / Nacos
🎯 负载均衡 → Ribbon / Feign
🎯 API 网关 → Spring Cloud Gateway
🎯 熔断限流 → Hystrix / Sentinel
🎯 监控 & 追踪 → Actuator + Zipkin
todo灰度发布
数据库 & 性能优化
在 Java 高级面试中,数据库和性能优化是重要的考察方向,涉及 数据库索引、SQL 调优、事务、锁机制、分库分表、分布式事务、缓存优化 等内容
1. 数据库索引
1.1 为什么要使用索引?
索引能够加快查询速度,减少 I/O 操作,提高数据库的整体性能。但会影响增删改的性能,因此需要权衡使用。
1.2 索引的底层数据结构
- B+ 树(最常见)
- 叶子节点存储数据,内部节点存储索引
- 非叶子节点不存储实际数据,占用空间小,提高查询效率
- 叶子节点之间提供有序的 双向链表,便于范围查询
- 哈希索引
- 适用于 确定值查询(=),不支持范围查找(BETWEEN, ORDER BY)
- 可能会发生 哈希冲突
- 常用于缓存
- 全文索引(Full-Text Index)
- 针对大文本字段(TEXT, VARCHAR)进行搜索
1.3 聚簇索引 vs 非聚簇索引
- 聚簇索引
- 数据与索引存储在一起,叶子节点直接存储数据
- 主键索引通常是聚簇索引(InnoDB)
- 优点:查询速度快(直接访问数据)
- 缺点:更新/插入数据时可能导致数据页分裂,影响性能
- 非聚簇索引
- 叶子节点存储的不是数据本身,而是主键的值
- 适用于辅助索引
1.4 覆盖索引、回表、索引下推
- 覆盖索引(Covering Index)
- 查询列已包含在索引中,不需要回表
SELECT id, name FROM user WHERE name='Tom';(如果 name 建立索引)
- 回表
- 当查询字段未包含在索引中,就需要通过主键索引查询完整的数据
SELECT id, name, age FROM user WHERE name='Tom';(需要回表查询 age)
- 索引下推(Index Condition Pushdown, ICP)
- 让存储引擎提前过滤数据,减少向 MySQL 服务器传输数据
2. SQL 调优
2.1 如何分析 SQL 语句的性能?
使用 EXPLAIN 查看 SQL 语句的执行计划:
EXPLAIN SELECT * FROM user WHERE name = 'Tom';
结果字段:
type:ALL(全表扫描) > index > range > ref > eq_ref > const > systemrows:扫描的行数(越少越好)key:使用的索引extra:Using index / Using where / Using FileSort(避免) / Using temporary(避免)
2.2 SQL 语句优化策略
- 避免
SELECT *,只查询需要的字段 - 避免
LIKE '%XX%',会导致全表扫描 - 合理使用索引,并避免索引失效
- 使用
JOIN代替多次SELECT - 分批查询大数据
LIMIT 10000,100
3. 事务(Transaction)
3.1 事务的四大特性(ACID)
- A(原子性):Atomicity
- 事务是一个整体,必须全部完成或全部失败
- 通过回滚(ROLLBACK)实现
- C(一致性):Consistency
- 数据不会出现不一致的状态
- 外键约束、唯一性约束保证一致性
- I(隔离性):Isolation
- 事务之间相互隔离,避免相互干扰
- D(持久性):Durability
- 事务提交后,数据持久化到磁盘
3.2 事务隔离级别
- 读未提交(Read Uncommitted)
- 允许脏读(Dirty Read)
- 读已提交(Read Committed)
- 避免脏读,但会导致不可重复读
- 可重复读(Repeatable Read,MySQL 默认)
- 避免不可重复读,MVCC 机制,可能发生幻读
- 串行化(Serializable)
- 全部事务串行执行,但性能最差
4. MySQL 锁
4.1 行锁 vs 表锁
- 行级锁(InnoDB)
- 适用于 高并发 场景
SELECT ... FOR UPDATE会产生行锁
- 表级锁(MyISAM)
- 适用于 读多写少
4.2 乐观锁 vs 悲观锁
- 乐观锁
- 适用于 读取多,写入少
- 通过
版本号或时间戳实现 UPDATE user SET balance=balance+100 WHERE id=1 AND version=2;
- 悲观锁
- 适用于 写多的场景
SELECT ... FOR UPDATE
5. 分库分表 & 分布式事务
5.1 为什么要分库分表?
- 单表数据量过大,查询性能下降
- 单个数据库的写入能力有限
- 提升系统的可扩展性
5.2 分库分表的策略
- 垂直分库
- 按 业务 进行拆分,例如
user库,order库
- 按 业务 进行拆分,例如
- 水平分库
- 订单量过大时,拆分
order_1~order_10
- 订单量过大时,拆分
- 水平分表
- 按 ID 取模:
uid % 10 = 3 --> order_3 表
- 按 ID 取模:
5.3 分布式事务解决方案
- 2PC(两阶段提交),但性能较差
- TCC(Try-Confirm-Cancel),保证事务一致性
- Seata(分布式事务框架)
- MQ 事务(基于消息补偿)
6. 缓存优化
6.1 为什么要使用缓存?
- 降低数据库压力,提高查询速度
- 减少数据库连接数,提高系统并发能力
6.2 使用 Redis 作为缓存
- 缓存穿透:查询不存在的 key,导致数据库压力过大
- 解决方案:布隆过滤器
- 缓存击穿:热点 key 过期,导致瞬间数据库压力暴增
- 解决方案:提前续期
- 缓存雪崩:大量缓存数据同时过期
- 解决方案:
- 设置不同过期时间
- 添加随机值
- 双层缓存
- 解决方案:
7.1 零拷贝(Zero Copy)
- 传统 IO:用户态 & 内核态 交互
- mmap、sendfile 高效 IO
在 高性能系统设计(如 Web 服务器、数据库、大文件传输) 中,提高 IO 效率 是至关重要的。零拷贝(Zero Copy) 允许数据 在硬件和网络传输过程中尽量减少 CPU 参与,提高吞吐量,降低内存拷贝 。
1. 传统 IO 模型
1.1 传统 IO 读取数据流程
在 传统 IO(read + write) 中,数据从磁盘读取到应用程序,再写入到网络,过程如下:
- read 调用
- 磁盘数据 → 内核缓冲区(Kernel Buffer)
- 拷贝到用户缓冲区(User Buffer)
- write 调用
- 用户缓冲区 → 再次拷贝到内核缓冲区
- 通过网卡 发送到客户端
📌 缺点:
- 内存拷贝(CPU 负担高):数据在内核态和用户态切换多次
- 系统调用(syscall)开销大:每次 read/write 都需要切换到内核态
- 数据复制了 2 次:磁盘 → 内核缓冲区 → 用户缓冲区,再写到内核缓冲区
2. 零拷贝技术(Zero Copy)
为优化 IO 和 CPU 性能,零拷贝 通过减少数据拷贝次数 & 用户态和内核态间的切换 提高系统性能。常见的零拷贝技术包括:
2.1 mmap + write(内存映射文件)
📌 原理:
mmap()将文件直接映射到进程的用户空间,省去 read 拷贝数据到用户缓冲区 的过程。
📌 流程:
- mmap() 在用户空间创建文件内存映射(磁盘 → 直接加载到内存)
- write() 直接从映射的用户内存区写入 socket(仅有 1 次拷贝)
📌 数据流动:
[磁盘] → [内核态 PageCache] → [用户态 mmap] → [内核 Socket 缓冲区] → [网卡]
📌 优缺点分析:
✔ 减少内存拷贝次数(少了一次 copy_to_user)
✔ 适用于 大文件处理,如 log 文件读取
❌ 依赖系统支持,可能 导致“缺页异常”(Page Fault) 影响性能
2.2 sendfile()(Linux 2.1 引入)
📌 原理:
sendfile()允许数据直接从内核缓冲区发送到 socket,避免read()+write()额外拷贝。
📌 流程:
- 文件内容加载到 PageCache
- 直接从 PageCache 读取到 Socket 缓冲区
- 网卡通过 DMA 直接读取
📌 数据流动:
[磁盘] → [内核缓冲区(PageCache)] → [Socket 发送缓冲区] → [网卡]
📌 优缺点分析:
✔ 减少一次数据拷贝(没有 copy_to_user)
✔ 减少 syscall 调用,提升性能
❌ 仍然经过 socket 缓冲区,占用 CPU 资源
2.3 sendfile + DMA(Linux 2.4+,TCP 网络优化)
📌 原理:
- 在
sendfile的基础上,利用 DMA(Direct Memory Access) 直接传输到网卡,无需 CPU 参与。
📌 流程:
- 磁盘数据 → 内核 PageCache
- DMA 直接从内核缓冲区发送到网卡( 零拷贝 Zero Copy)
- CPU 完全不参与数据搬运,由 DMA 处理
📌 数据流动(O_DIRECT + sendfile + DMA):
[磁盘] → [PageCache] → [DMA 直传网卡]
📌 优缺点分析:
✔ 零 CPU 复制,完全零拷贝(减少 CPU 使用率,降低系统开销)
✔ 适用于高吞吐量的服务器,例如 Nginx、Kafka、数据库
❌ 需要网卡支持 DMA 加速(现代服务器已支持)
3. 零拷贝技术对比
| 技术 | 是否 CPU 复制 | 是否用户态拷贝 | 适用场景 |
|---|---|---|---|
| 传统IO (read + write) | ✅ 2次 CPU 拷贝 | ✅ 需要 | 适用于普通文件 IO |
| mmap + write | ✅ 1次拷贝 | ❌ 省去用户缓冲区 | 适用于大块数据 |
| sendfile | ✅ 1次拷贝 | ❌ 省去用户缓冲区 | 适用于网络传输 |
| sendfile + DMA(最新) | ❌ 0拷贝(完全零拷贝) | ❌ 省去用户缓冲区 | 适用于Kafka、Nginx、数据库 |
4. 零拷贝的应用场景
4.1 高吞吐量的 Web 服务器(Nginx、Apache)
- Nginx 默认使用
sendfile()来加速static file传输,可配合 TCP zero-copy 优化:sendfile on; # 开启 sendfile tcp_nopush on; # 避免小包发送 - 效果:
- 减少 CPU 负担
- 提高 Web 服务器并发吞吐量
4.2 Kafka(日志存储与传输)
Kafka 直接采用 FileChannel.transferTo()(底层 sendfile)
FileChannel channel = file.getChannel();
channel.transferTo(0, file.length(), socketChannel);
- 减少 70% 以上的 CPU 使用
- 支持百万级 TPS 日志吞吐
4.3 MySQL/InnoDB 零拷贝优化
- mmap 加速索引扫描
- O_DIRECT 直接绕过 PageCache,避免 OS 额外拷贝
5. 总结
💡 传统 IO(read + write)的问题:
- CPU 需要反复 拷贝数据,占用大量资源
- 内核 & 用户态 多次切换,产生系统开销
💡 Zero Copy 方案比较:
| 方案 | 优势 | 适用场景 |
|---|---|---|
| mmap+write | 适用于大文件 | 日志处理 |
| sendfile | 适用于网络传输 | Web 服务器 |
| sendfile+DMA | 完全零拷贝,减少 CPU 负载 | Kafka、Nginx、MySQL |
💡 如何在面试中回答 Zero Copy?
✅ 什么是零拷贝?
✅ sendfile 和 mmap+write 的区别?
✅ Kafka、Nginx 如何使用零拷贝?
✅ MySQL 如何利用零拷贝优化?
总结
☑ 数据库索引:B+ 树、覆盖索引、回表查询
☑ SQL 调优:EXPLAIN 分析查询计划
☑ 事务隔离级别:ACID、MVCC
☑ 锁机制:行锁 vs 表锁,乐观锁 vs 悲观锁
☑ 分库分表 & 分布式事务:Sharding、Seata
☑ 缓存优化:Redis 解决缓存雪崩、穿透、击穿
Dubbo
知道什么是 RPC 么?
RPC 就是 Remote Procedure Call,远程过程调用,它相对应的是本地过程调用
那为什么要有 RPC,HTTP 不好么?
先提出这两个不是一个层级的东西,没有可比性,HTTP 协议比较冗余,所以 RPC 大多都是基于 TCP 自定义协议,定制化的才是最适合自己的,
说说你对 Dubbo 的了解
历史发展 : Dubbo 是阿里巴巴开源的一个基于 Java 的 RPC 框架,中间沉寂了一段时间,但在 2017 年阿里巴巴又重启了对 Dubbo 维护。并且在 2018 年和 当当的 Dubbox 进行了合并,进入 Apache 孵化器,在 2019 年毕业正式成为 Apache 顶级项目。目前 Dubbo 社区主力维护的是 2.6.x 和 2.7.x 两大版本,2.6.x 版本主要是 bug 修复和少量功能增强为准,是稳定版本。2.7.5 版本的发布被 Dubbo 认为是里程碑式的版本发布,支持 gRPC,并且性能提升了 30%(这里不了解gRPC 和为什么性能提升的话就别说了,别给自己挖坑)。最新的 3.0 版本往云原生方向上探索着。
总体架构:
| 节点 | 角色说明 |
|---|---|
| Consumer | 需要调用远程服务的服务消费方 |
| Registry | 注册中心 |
| Provider | 服务提供方 |
| Container | 服务运行的容器 |
| Monitor | 监控中心 |
首先服务提供者 Provider 启动然后向注册中心注册自己所能提供的服务。服务消费者 Consumer 启动向注册中心订阅自己所需的服务。然后注册中心将提供者元信息通知给 Consumer, 之后 Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用
看过源码,那说下服务暴露的流程?
服务的暴露起始于 Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL, 然后根据 URL 的参数来进行本地或者远程调用。
会通过 proxyFactory.getInvoker,利用 javassist 来进行动态代理,封装真的实现类,然后再通过 URL 参数选择对应的协议来进行 protocol.export,默认是 Dubbo 协议。
在第一次暴露的时候会调用 createServer 来创建 Server,默认是 NettyServer。
然后将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找,然后会向注册中心注册提供者的信息。
基本上就是这么个流程
看过源码,那说下服务引入的流程?
服务的引入时机有两种,第一种是饿汉式,第二种是懒汉式。
饿汉式就是加载完毕就会引入,懒汉式是只有当这个服务被注入到其他类中时启动引入流程,默认是懒汉式。
会先根据配置参数组装成 URL ,一般而言我们都会配置的注册中心,所以会构建 RegistryDirectory 向注册中心注册消费者的信息,并且订阅提供者、配置、路由等节点。
得知提供者的信息之后会进入 Dubbo 协议的引入,会创建 Invoker ,期间会包含 NettyClient,来进行远程通信,最后通过 Cluster 来包装 Invoker,默认是 FailoverCluster,最终返回代理类。
看过源码,那说下服务调用的流程?
调用某个接口的方法会调用之前生成的代理类,然后会从 cluster 中经过路由的过滤、负载均衡机制选择一个 invoker 发起远程调用,此时会记录此请求和请求的 ID 等待服务端的响应。
服务端接受请求之后会通过参数找到之前暴露存储的 map,得到相应的 exporter ,然后最终调用真正的实现类,再组装好结果返回,这个响应会带上之前请求的 ID。
消费者收到这个响应之后会通过 ID 去找之前记录的请求,然后找到请求之后将响应塞到对应的 Future 中,唤醒等待的线程,最后消费者得到响应,一个流程完毕。
知道什么是 SPI 嘛?
SPI 是 Service Provider Interface,主要用于框架中,框架定义好接口,不同的使用者有不同的需求,因此需要有不同的实现,而 SPI 就通过定义一个特定的位置,Java SPI 约定在 Classpath 下的 META-INF/services/ 目录里创建一个 以服务接口命名的文件 ,然后 文件里面记录的是此 jar 包提供的具体实现类的全限定名 。所以就可以通过接口找到对应的文件,获取具体的实现类然后加载即可,做到了灵活的替换具体的实现类
为什么 Dubbo 不用 JDK 的 SPI,而是要自己实现?
问这个问题就是 看你有没有深入的了解,或者自己思考过 ,不是死板的看源码,或者看一些知识点。
答:因为 Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且 将实现类全部实例化 ,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
因此 Dubbo 就自己实现了一个 SPI,给每个实现类配了个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。
Dubbo 为什么默认用 Javassist
上面你回答 Dubbo 用 Javassist 动态代理,所以很可能会问你为什么要用这个代理,可能还会引申出 JDK 的动态代理、ASM、CGLIB。
所以这也是个注意点,如果你不太清楚的话上面的回答就不要扯到动态代理了,如果清楚的话那肯定得提,来诱导面试官来问你动态代理方面的问题,这很关键。
面试官是需要诱导的 ,毕竟他也想知道你优秀的方面到底有多优秀,你也取长补短,双赢双赢。
来回答下为什么用 Javassist,很简单, 就是快,且字节码生成方便 。
ASM 比 Javassist 更快,但是没有快一个数量级,而Javassist 只需用字符串拼接就可以生成字节码,而 ASM 需要手工生成,成本较高,比较麻烦。
如果让你设计一个 RPC 框架,如何设计?
可以从底层向上开始说起 。
首先需要实现高性能的网络传输,可以采用 Netty 来实现,不用自己重复造轮子,然后需要自定义协议,毕竟远程交互都需要遵循一定的协议,然后还需要定义好序列化协议,网络的传输毕竟都是二进制流传输的。
然后可以搞一套描述服务的语言,即 IDL(Interface description language),让所有的服务都用 IDL 定义,再由框架转换为特定编程语言的接口,这样就能跨语言了。
此时最近基本的功能已经有了,但是只是最基础的,工业级的话首先得易用,所以框架需要把上述的细节对使用者进行屏蔽,让他们感觉不到本地调用和远程调用的区别,所以需要代理实现。
然后还需要实现集群功能,因此的要服务发现、注册等功能,所以需要注册中心,当然细节还是需要屏蔽的。
最后还需要一个完善的监控机制,埋点上报调用情况等等,便于运维
Kafka
Kafka 作为分布式消息队列的核心组件,面试过程中经常考察 架构原理、分区、副本存储、高可用机制、生产者消息确认、Offset 提交与消费、性能调优、可靠性保证 等知识点。本文将详细剖析 Kafka 的核心知识点,覆盖 底层原理 + 源码 + 生产应用场景
Kafka 是 基于 Topic 的消息分发架构,它的核心组件包括:
- Producer(生产者) 发送消息到 Kafka。
- Broker(Kafka 服务器) 负责存储消息。
- Topic(主题) 控制消息分类,一个 Topic 下有多个 Partition(分区)。
- Consumer(消费者) 订阅 Topic 并消费消息。
- Consumer Group(消费者组) 共享一个 Topic 的消费任务,每个分区 只能被同一个 Group 内的一个 Consumer 消费。
- Zookeeper(注册中心) 负责 Broker 管理、Leader 选举。
1. Kafka 分区机制
1.1 为什么 Kafka 需要分区(Partition)
Kafka 以分区为单位存储消息,引入 Partition 主要有三个目的:
- 水平扩展 Kafka 集群 —— 一个 Topic 内的 Partition 可以分布在不同的 Broker,提高性能。
- 支持并发消费 —— 每个 Consumer 只能消费某个 Partition,提高吞吐量。
- 保证 Partition 内部消息有序性 —— 某个 Partition 内部是顺序写的,不同 Partition 之间是乱序的。
1.2 Kafka 生产者如何选择分区
Kafka 生产者发送消息时,会根据 Partition 选择策略 决定消息存入哪个分区:
- ✅ 指定 Partition ID(直接选择 Partition)
- ✅ 基于 Key 哈希分区(
hash(key) % Partition 数) - ✅ 均衡轮询(无 Key 时采用 Round Robin)
- ✅ 自定义
Partitioner策略
// 自定义分区策略
public class CustomPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
int partitionNum = cluster.partitionsForTopic(topic).size();
return (key.hashCode() & Integer.MAX_VALUE) % partitionNum;
}
}
面试追问:如果 Kafka Partition 设置太多会有什么问题?
- 🚨 文件句柄过多:每个 Partition 都会维护多个日志文件(
log.segment.bytes)。- 🚨 ISR 维护成本增加:Leader 需要同步多个 Follower,影响 Kafka 复制性能。
2. Kafka 如何保证高可用?
Kafka 采用 多副本机制 (replica),提升数据可靠性:
- 每个 Partition 会有 多个副本(replica),其中 一个 leader 负责读写,其余 follower 只负责同步数据。
- ISR(In-Sync Replica):Kafka 维护同步副本集合,确保副本同步不落后。
💡 复习点:Kafka 如何选举 Leader?
- Kafka 由 Zookeeper 负责选举 Partition 的 Leader。
- 如果 Leader 宕机,系统会 从 ISR 里选一个 Follower 作为新的 Leader。
- 如果 ISR 为空,Kafka 可能会丢失数据(除非
unclean.leader.election.enable=false)。
📌 Leader 选举源码
private PartitionState leaderElection() {
Broker chosenReplica = selectFromISR();
return new PartitionState(chosenReplica);
}
面试追问:Kafka 为什么要用 ISR 而不是 AR(All Replica)?
- ISR:只要求同步副本集,保证低延迟和高性能。
- AR(All Replicas):必须所有副本同步才确认,严格但影响 Kafka 吞吐。
3. Kafka 生产者消息确认机制
Kafka Producer 发送消息时,可通过 acks 参数控制 消息可靠性:
acks 值 |
吞吐量 | 数据丢失概率 | 适用场景 |
|---|---|---|---|
acks=0 |
最高 | 高(不等待 broker 确认) | 性能优先(监控日志) |
acks=1(默认) |
高 | 可能丢数据(Leader 宕机会丢失) | 业务默认选项 |
acks=all(-1) |
低 | 最安全(ISR 全部确认) | 交易支付、金融业务 |
properties.put(ProducerConfig.ACKS_CONFIG, "all");
📢 面试追问:Kafka 如何防止消息重复?
- 幂等机制 (
enable.idempotence=true):保证同一 Producer MessageId 只被提交一次。 - Producer 事务支持(
transactional.id):确保 Producer 端消息提交或回滚。 - 去重逻辑(消费端幂等处理):业务级
msg_id去重。
4. Kafka 消费者如何存储 Offset
Kafka 消费者(Consumer)消费数据时会跟踪 Offset,有两种方式:
- 自动提交(默认):
- ✅ 每 5s 提交 Offset
- ❌ 可能会导致消息丢失或重复消费
- 手动提交(推荐):
- 🚀
commitSync()(同步提交,保证准确,但性能差) - 🚀
commitAsync()(异步提交,性能好但失败可能丢失)
- 🚀
consumer.commitSync(); // 确保 offset 递增更新
面试追问:Kafka Consumer 端如何防止消息丢失?
- 关闭自动提交 (
enable.auto.commit=false)- 手动提交 offset,防止读取异常导致重复消费
- 使用
__consumer_offsets主题存储 offset 位置
5. Kafka 性能优化
5.1 生产者优化
- 批量发送 (
batch.size):超过一定大小才发送,减少网络压力。 - 数据压缩 (
compression.type=gzip/lz4/snappy) 提高吞吐。 - 消息合并发送 (
linger.ms) 控制延迟,提高吞吐量。
5.2 消费者优化
- Partition 充分利用:提高分区数,增加 Consumer 并行消费能力。
- Fetch 大批量拉取数据:减少网络 I/O。
- 手动提交 Offset,避免消息重复消费。
5.3 Broker 端优化
- 日志文件分片 (
log.segment.bytes) 避免存储压力。 - 清理策略 (
log.retention.hours) 设置合理日志过期时间,减少磁盘占用。
总结
Kafka 面试常考:
- Kafka 分区机制如何提高吞吐?
- Kafka 生产者如何选择分区?
- Kafka 副本同步机制如何保证数据可靠?
- Kafka 幂等写入如何保证 Exactly Once?
- Kafka 高可用策略(Leader 选举 & 分区副本)
- Kafka 调优技巧(生产者、消费者、Broker)
Sharding-JDBC
简述
1. Sharding-JDBC 是什么?它的作用是什么?
回答:
Sharding-JDBC 是 Apache ShardingSphere 中的一个子项目,它是 轻量级的 Java 数据库分片中间件,以 JDBC 代理层 的形式嵌入到应用程序中,提供 分库分表、分布式事务、数据治理等功能,对上层业务透明。
主要作用:
- 数据库分片(水平分库分表,提升可扩展性)
- 读写分离(提高读性能)
- 分布式事务(基于 Seata、XA 等模式)
- 数据治理(SQL 解析、影子库、流量管理等)
2. Sharding-JDBC 是如何实现分库分表的?
回答:
Sharding-JDBC 主要依赖 SQL 解析 + 路由计算 + SQL 改写 + SQL 执行 + 结果归并 来完成分片查询。核心组件:
- SQL 解析:解析 SQL 语句,提取 SQL 结构和分片键。
- 路由计算:根据分片策略计算目标库表。
- SQL 改写:将逻辑 SQL 改写成物理 SQL(适配多个分片数据库)。
- SQL 执行:根据计算出的路由执行 SQL。
- 结果归并:合并分片数据库返回的结果,并进行排序、分组等优化。
3. 如何配置 Sharding-JDBC 实现分库分表?
回答:
使用 Sharding-JDBC 需要在 YAML / Java 配置 中定义 分片规则。
示例 YAML 配置(ShardingSphere 5.x):
rules:
- !SHARDING
tables:
user:
actualDataNodes: ds${0..1}.user_${0..1} # 2 个库,每个库 2 张表
tableStrategy:
standard:
shardingColumn: id
shardingAlgorithmName: table-inline
databaseStrategy:
standard:
shardingColumn: id
shardingAlgorithmName: database-inline
shardingAlgorithms:
table-inline:
type: INLINE
props:
algorithm-expression: user_${id % 2}
database-inline:
type: INLINE
props:
algorithm-expression: ds${id % 2}
4. 怎么选择 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar?
回答:
| 方案 | 适用场景 | 方式 | 适用架构 |
|---|---|---|---|
| Sharding-JDBC | 轻量级方案,无需额外中间件 | 应用程序直接嵌入 | 适用于 SpringBoot、MyBatis |
| Sharding-Proxy | 分布式数据库网关,支持多语言 | 代理数据库,基于 MySQL/PostgreSQL 协议 | 替代 MySQL 代理(Mycat 方案) |
| Sharding-Sidecar | 云原生方案(Kubernetes 微服务架构) | 以 Sidecar 形式运行 | 适用于 Service Mesh 架构 |
5. Sharding-JDBC 支持哪些分片策略?
回答:
-
标准分片(单分片键)
- INLINE(行表达式计算)
- HINT(强制指定路由)
- Complex(多个分片键)
- CLASS_BASED(自定义 Java 逻辑)
-
范围分片(Range)
- 适用于 BETWEEN AND 操作
-
哈希分片
- 例如:Mod(取模分片)
-
绑定表分片
- 例如:
order和order_item绑定时,ID 需要保持一致。
- 例如:
6. Sharding-JDBC 支持哪些分布式事务?
回答:
- 本地事务(默认)
- 柔性事务
- 基于 最大努力送达(Best Efforts Delivery)
- 基于 TCC(Try-Confirm-Cancel)
- XA 分布式事务
- 通过 Atomikos、Narayana、Seata 实现
7. Sharding-JDBC 如何实现读写分离?
回答:
Sharding-JDBC 提供 主从复制(读写分离),可以配置主库进行写操作,从库执行读操作。
示例 YAML 配置:
rules:
- !READWRITE_SPLITTING
dataSources:
rw_ds:
writeDataSourceName: ds_master
readDataSourceNames:
- ds_slave
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
其中:
writeDataSourceName指定主库readDataSourceNames指定从库ROUND_ROBIN轮询策略
8. Sharding-JDBC 的分布式 ID 解决方案有哪些?
回答:
Sharding-JDBC 提供 雪花算法(Snowflake) 作为 分布式唯一 ID 生成器。
使用方式:
shardingAlgorithms:
snowflake:
type: SNOWFLAKE
props:
worker-id: 123
- 避免主键冲突
- 适用于分布式数据库方案
其他分布式 ID 方案:
- UUID
- 数据库自增 ID(不推荐)
- Redis ID 生成
- Twitter Snowflake
- Leaf(美团开源)
9. 在 Sharding-JDBC 中,哪些 SQL 语句不支持?
回答:
Sharding-JDBC 由于是基于 SQL 解析的,所以 不支持某些复杂 SQL:
- 多表
JOIN操作(尤其是分库场景) - 子查询复杂 SQL
UPDATE/DELETE跨库事务操作- 跨库
ORDER BY+LIMIT性能较差 GROUP BY需要手动优化或由应用层处理
10. 如何优化 Sharding-JDBC 查询性能?
回答:
- 合理设计分片键:选择查询频率较高的字段,如
order_id、user_id作为分片键。 - 尽量避免
JOIN查询:方案是通过应用合并数据或使用bindingTable。 - 规范 SQL,避免
SELECT *,减少返回数据量。 - 合理使用分片策略,减少单查询跨库操作。
- 开启读写分离,提高读查询效率。
- 使用异步查询,减少等待时间,如
Sharding-Sphere-Proxy提供流式查询支持。
总结
Sharding-JDBC 提供 分库分表、读写分离、分布式事务、多种分片策略 等功能,是 高并发、海量数据场景 下的优秀方案。但在使用时要 合理设计分片策略,避免分片带来的 SQL 限制问题。
以上问题涵盖 核心概念、配置方式、事务支持、读写分离、性能优化,是 Sharding-JDBC 面试高频考点,建议深入理解源码和原理。
高级
1. Sharding-JDBC 内部执行流程是怎样的?
Sharding-JDBC 的 SQL 执行过程分为 5 大核心步骤:
-
SQL 解析(SQL Parsing)
- 解析 SQL 语句的结构,识别查询表、字段、where 条件、排序等信息。
- 使用 ANTLR4 进行 SQL 解析,获取 分片键(Sharding Key) 信息。
-
SQL 路由(SQL Routing)
- 根据 分库分表规则 计算 SQL 需要执行的数据库节点。
- 分为:
- 标准路由(Standard Routing):基于分片键计算目标库表。
- 广播路由(Broadcast Routing):SQL 必须在所有分片库中执行(如
DDL)。 - Hint 路由(强制路由):通过 Hint 指定目标数据库。
-
SQL 改写(SQL Rewrite)
-
将逻辑 SQL 解析为目标物理 SQL,如:
SELECT * FROM user WHERE id = 10;=>
SELECT * FROM user_1 WHERE id = 10;
-
-
SQL 执行(SQL Execution)
- 多数据源管理,路由 SQL 到具体数据库执行。
- 并行执行优化(多个数据库分库并发查询)。
-
结果合并(Result Merging)
- 多个数据库返回查询结果后,Sharding-JDBC 统一合并,并进行:
- 排序(Order By)
- 分页(Limit)
- 分组(Group By)
- 去重(Distinct)
- 多个数据库返回查询结果后,Sharding-JDBC 统一合并,并进行:
📝 示例 SQL
SELECT id, name FROM t_order WHERE id IN (1, 2, 3) ORDER BY create_time LIMIT 3;若
t_order水平拆分为t_order_0和t_order_1:
- 执行
t_order_0和t_order_1的子查询- 结果合并后再应用 Order By + Limit
2. Sharding-JDBC 的 Hint 强制路由是如何实现的?
Hint(暗示查询)模式用于 无法通过 SQL 确定分片键 的情况,如:
- 无 WHERE 查询(随机库选取)
- 聚合查询(COUNT/AVG)
- 手动选择目标库
📌 Hint 语法
HintManager hintManager = HintManager.getInstance();
hintManager.setDatabaseShardingValue(1); // 选择 ds1
hintManager.setTableShardingValue(2); // 选择 table_2
📌 Hint 底层实现
- 通过 Hint 数据结构(ThreadLocal 变量) 存储手动分片信息。
- SQL 执行时提取 Hint 信息,覆盖 SQL 解析出的分片键。
- 强制路由到指定库表。
3. Sharding-JDBC 支持哪些分布式事务?深入剖析!
Sharding-JDBC 提供 3 类事务:
-
本地事务(Local Transaction)
- 适用于 单库事务,如 MySQL 事务的
commit和rollback。
- 适用于 单库事务,如 MySQL 事务的
-
XA 分布式事务(严格两阶段提交)
- 支持原子性 ACID,但性能损耗较大。
- 使用 Atomikos、Narayana。
- 劣势:
- 需要数据库支持 XA
- 性能受全局锁和两阶段提交影响
-
BASE 柔性事务
- 最大努力送达(Best Effort Delivery)
- 事务补偿,失败后不断重试。
- TCC 事务(Try Confirm Cancel)
- 适用于微服务架构。
- 最大努力送达(Best Effort Delivery)
XA 实践案例
配置示例:
rules:
- !TRANSACTION
defaultType: XA
providerType: Atomikos
代码示例:
XAConnection xaConn = new MysqlXADataSource().getXAConnection();
XAResource xaResource = xaConn.getXAResource();
xaResource.start(xid, TMNOFLAGS);
深入理解:
XA 事务采用 2PC(Two-Phase Commit)
- 第一阶段(Prepare)
- 事务执行,但不提交(加锁)
- 所有事务都执行成功后,进入 Prepare 状态
- 第二阶段(Commit)
- 如果所有分支事务都
Prepare成功,发送Commit - 如果任何事务
Prepare失败,则Rollback
- 如果所有分支事务都
问题 & 优化
- 全局锁开销大:可以使用 TCC 替代
- 网络开销大:可以通过 存储事务日志 提高可靠性
- 数据库支持问题:某些数据库对 XA 兼容性较差
4. Sharding-JDBC 如何处理一致性及分布式 ID ?
由于 Sharding-JDBC 可能分库,表的主键 ID 不能依赖数据库自增 ID,因此需要 分布式 ID。
📌 分布式 ID 方案
- UUID(不推荐)
- 长度大,影响索引效率
- 数据库自增 ID(不推荐)
- 跨数据库无法保证全局唯一
- Twitter Snowflake
- 高性能,高并发
- 可以保证递增趋势(分布式索引友好)
workerID需要动态分配
- Leaf(美团开源方案)
- Leaf Segment 模式
- Leaf Snowflake 模式
- Redis 自增 ID
INCR方案- 但 Redis 可能出现单点故障问题
📌 Sharding-JDBC 内置 ID 方案
Sharding-JDBC 内部集成了 雪花算法:
shardingAlgorithms:
snowflake:
type: SNOWFLAKE
props:
worker-id: 123
String key = ShardingKeyGenerator.generateKey();
- 采用 41-bit 时间戳 + 10-bit 机器 ID + 12-bit 计数器
- 高性能,每秒可生成数百万个 ID
- 确保 全局唯一性
5. Sharding-JDBC 分片路由优化
📌 路由优化方案
-
合理选择分片键
- 选择高查询频率字段(如
user_id) - 避免跨库
JOIN
- 选择高查询频率字段(如
-
提前过滤 SQL
- 避免
SELECT * - 使用
SQL 预解析限制跨表 Join
- 避免
-
绑定表机制
order与order_item绑定,保证请求走同一数据库:
binding-tables: - order, order_item -
广播表优化
dict、config等小表可在所有库中创建,减少 Join 查询跨库问题。
总结
Sharding-JDBC 是 分布式数据库架构的关键组件,在面试中你可以从:
- 执行原理(SQL 解析、路由、重写等)
- 分布式事务(XA/TCC)
- 分布式 ID 设计
- 数据一致性
- 优化方案(路由、JOIN、读写分离)
等方面深入展现你的理解。
Elasticsearch
Elasticsearch简述
1. 什么是 Elasticsearch?
答案:
Elasticsearch 是一个基于 Apache Lucene 构建的分布式搜索和分析引擎。它提供了 RESTful API,支持 全文搜索、结构化检索、聚合分析、分布式存储 等功能,常用于 日志分析、电子商务搜索、数据分析等应用场景。
2. Elasticsearch 的核心概念有哪些?
答案:Elasticsearch 的核心概念如下:
- Index(索引):相当于关系型数据库中的一个数据库,它是存储 文档(document)的集合。
- Type(类型)(ES 6.x 及之前) :类似于数据库中的表(Table),但 ES 7.x 之后取消了 Type。
- Document(文档):最基本的数据存储单位,JSON 格式存储。类似于数据库中的一条记录。
- Field(字段):文档中的属性,相当于数据库中表的列(column)。
- Node(节点):Elasticsearch 集群中的一个服务器实例。
- Cluster(集群):多个节点组成的 Elasticsearch 实例集合。
- Shard(分片):索引可以被分割为多个分片(shard),每个分片是一个独立的全功能 Lucene 索引。
- Replica(副本):每个主分片(Primary Shard)可以有多个副本(Replica Shard),提供高可用性。
3. ES 如何实现高可用?
答案:Elasticsearch 通过 分片(Sharding)和副本(Replication) 机制实现高可用性:
- 主分片(Primary Shard):用于存储索引的主要数据。
- 副本分片(Replica Shard):主分片的备份,可提供容错与负载均衡。
高可用性的实现:
- 节点故障恢复:如果某个节点宕机,Elasticsearch 会从可用副本中提升一个副本为主分片。
- 负载均衡:读请求可以通过副本提高性能。
- 分片自动分配:Elasticsearch 具有 自动分片管理 机制,可以在集群中动态分配分片。
4. Elasticsearch 如何处理海量数据索引?
答案:Elasticsearch 处理海量数据的方式包括:
- 分片(Sharding)+ 副本(Replica) 机制。
- 倒排索引(Inverted Index) 高效支持全文搜索。
- 索引优化(Index Lifecycle & Rollover):使用 time-based 索引,例如按天/小时分索引,自动清理旧索引。
- 批量写入(Bulk API) 提高索引效率,减少请求开销。
- 异步刷新(Translog + Segment Merge):ES 采用 写入先进入 translog(事务日志),再合并到索引,提高稳定性和查询性能。
5. Elasticsearch 有哪些数据类型?
答案:Elasticsearch 的数据类型主要包括:
-
字符串类型
text:适用于全文搜索,会进行 分词 处理。keyword:适用于精确匹配,不会分词。
-
数值类型
integer、long、short、byte、double、float、half_float。
-
时间类型
date:支持yyyy-MM-dd HH:mm:ss格式。
-
布尔类型
boolean(true/false)。
-
数组类型
array:支持存储多个值。
-
对象类型
object:用于定义嵌套结构。
-
嵌套类型
nested:更高级的对象类型,避免错位查询问题。
6. Elasticsearch 如何进行分页查询?
答案:
Elasticsearch 采用 from 和 size 进行分页,类似于 LIMIT offset, size 的 SQL 语法。
GET my_index/_search
{
"from": 10,
"size": 20,
"query": {
"match_all": {}
}
}
但是,from + size 方式在 from 很大时会有性能问题,可改用 深度分页 Scroll、Search After 方式。
7. 如何优化 Elasticsearch 查询性能?
答案:优化 Elasticsearch 查询性能的方式包括:
- 减少索引分片数量,避免分片过小或过大。
- 使用 Filter 代替 Query,减少打分计算。
- 批量查询(Bulk API),减少请求次数。
- 使用
doc_values代替_source进行聚合查询,提高效率。 - 使用
Search After替代深度分页,避免from+size造成的内存压力。 - 适当调整缓存(Request Cache、Field Data Cache、Query Cache),提高查询速度。
8. 什么是 IK 分词器,如何使用?
答案:IK 分词器(IK Analyzer)是 中文分词插件,支持:
- 智能分词(ik_smart):速度快,分词粗略。
- 细粒度分词(ik_max_word):适用于搜索,生成更多分词。
安装 IK 分词插件
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/vX.X.X/elasticsearch-analysis-ik-X.X.X.zip
配置索引映射
PUT my_index
{
"settings": {
"analysis": {
"analyzer": {
"ik_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
分词测试
GET _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
9. 怎么在 Elasticsearch 中删除数据?
答案:Elasticsearch 提供多种删除数据的方式:
- 删除单个文档
DELETE my_index/_doc/1
- 删除符合条件的文档
POST my_index/_delete_by_query
{
"query": {
"match": {
"title": "Elasticsearch"
}
}
}
- 删除索引
DELETE my_index
10. Elasticsearch 和 SQL 数据库的区别是什么?
| 对比项 | Elasticsearch | SQL 数据库 |
|---|---|---|
| 数据存储 | JSON 文档 | 结构化表 |
| 查询语言 | DSL(Domain Specific Language) | SQL |
| 索引方式 | 倒排索引优化全文搜索 | B+ 树一般查询 |
| 数据一致性 | 最终一致性 | 强一致性 |
| 适用场景 | 搜索、日志分析、大数据存储 | 事务型业务 |
进阶
1. 海量数据处理
Elasticsearch 设计用于处理海量数据,核心机制包括 分片(Sharding)、副本(Replication)、压缩存储、冷热数据管理。
(1) 主要技术
✅ 分片(Sharding)+ 副本(Replication)
- 主分片(Primary Shard) 负责索引数据,并分摊写入压力。
- 副本分片(Replica Shard) 提供数据冗余,提高查询性能。
✅ 倒排索引(Inverted Index)
- 全文搜索计算基于 倒排索引,而不是逐行扫描,提高查询速度。
✅ 索引生命周期管理(ILM)
- 冷热分离:冷数据存储到低成本节点。
- 索引 Rollover:定期创建新索引以维持查询性能。
(2) 优化策略
✅ 减少分片数量
- 分片过多会导致集群管理开销增加,建议每个索引分片大小 控制在 30GB 以内。
✅ 批量索引(Bulk API)
POST _bulk
{ "index": { "_index": "my_logs", "_id": "1" } }
{ "message": "log data", "timestamp": "2024-06-10T12:00:00" }
{ "index": { "_index": "my_logs", "_id": "2" } }
{ "message": "log data2", "timestamp": "2024-06-10T12:01:00" }
为什么用 Bulk?
- 降低请求开销,减少 IO 及网络压力。
✅ 减少 _source 存储
PUT my_index
{
"mappings": {
"properties": {
"name": { "type": "keyword", "index": true, "store": true }
}
}
}
_source默认存储 JSON,禁用_source可节省大约 30% 存储空间。
2. 主节点职责
Elasticsearch 采用 主从架构(Master-Slave),其中主节点(Master Node)负责:
- 维护集群元数据(Cluster Metadata)。
- 进行索引创建、删除、分片分配、节点管理等操作。
(1) 具体职责
✅ 分片分配(Shard Allocation)
- 当新节点加入或故障恢复时,主节点负责重新分配分片。
✅ 索引管理
- 创建、删除索引,执行
PUT / DELETE / _settings操作。
✅ 集群健康监测
- 监控主从节点状态,对不健康节点执行切换。
(2) 多主节点架构
防止脑裂(Split-Brain)
- 最佳实践建议至少 3 个 Master 节点,并设置:
PUT _cluster/settings
{
"persistent": { "cluster.routing.allocation.enable": "all" }
}
minimum_master_nodes = N/2 + 1(N为 master 节点数)。
3. 分页优化(Search After & Scroll)
ES 默认支持 from + size 分页机制,但数据量大时会导致性能下降:
from: 10000, size: 10需要扫描 10,010 条数据,浪费查询性能。
(1) Search After 高效分页
Search After 适用于基于时间或 ID 的分页,避免性能损耗。
GET my_index/_search
{
"size": 100,
"sort": [{ "timestamp": "asc" }],
"search_after": [1698345600]
}
💡 优点:不依赖 from,适用于实时数据。
(2) Scroll 深度分页
适用于 批量导出数据,而非前端分页。
GET my_index/_search?scroll=1m
{
"size": 500,
"query": { "match_all": {} }
}
scroll保持光标 1 分钟,允许分批拉取数据。
4. 分片均衡(Shard Balancing)
默认情况下,ES 自动将分片均匀分布在节点上,但某些情况需要手动优化。
(1) 监控分片
GET _cat/shards?v
- 一旦发现某个节点负载过重,可以手动迁移:
POST _cluster/reroute
{
"commands": [{
"move": {
"index": "logs",
"shard": 0,
"from_node": "node_1",
"to_node": "node_2"
}
}]
}
(2) 限制单节点最大分片数
PUT _cluster/settings
{
"persistent": { "cluster.routing.allocation.node_concurrent_recoveries": 2 }
}
5. 高可用架构
Elasticsearch 通过以下方式实现高可用(HA):
✅ 多 Master
✅ 副本 Shard
✅ 跨集群复制(CCR)
(1) 副本策略
PUT my_index/_settings
{
"index.number_of_replicas": 2
}
- 允许副本分布到不同节点,实现高可用。
(2) 关闭 Split-Brain
确保:
discovery.zen.minimum_master_nodes: 2
防止多个主节点出现分裂。
6. 查询优化
(1) term vs match 查询
term 适用于 结构化数据,match 适用于 全文搜索,避免误用影响性能:
GET my_index/_search
{
"query": {
"term": { "status": "active" }
}
}
✅ 使用 filter 避免评分计算
GET my_index/_search
{
"query": {
"bool": {
"filter": { "term": { "status": "active" } }
}
}
}
✅ 索引时移除 _source
PUT my_index
{
"mappings": {
"properties": {
"title": { "type": "text", "index": true },
"content": { "type": "text", "index": true, "_source": false }
}
}
}
7. 集群健康监控
ES 提供 多种方式监控集群健康:
(1) 关键 API
✅ 查看集群健康
GET _cluster/health
✅ 监控节点状态
GET _nodes/stats
✅ 查看 JVM 内存占用
GET _nodes/stats/jvm
(2) 工具
✅ ELK Stack - Elasticsearch X-Pack
✅ Grafana + Prometheus
✅ Filebeat 监控日志
最终总结
Elasticsearch 的性能优化、高可用、集群监控、查询优化 至关重要,面试时可以围绕以下 7 个方向回答:
✅ 海量数据存储
✅ Master 节点职责
✅ 分页优化(避免深度分页)
✅ Shard 负载均衡
✅ 高可用架构(Master + Replica)
✅ 查询优化(缓存、索引优化)
✅ 集群监控(X-Pack、Prometheus、API)
希望此篇 ES 高级面试指南 对你有所帮助! 🚀🚀🚀
MongoDB
基础
1. 什么是 MongoDB?
MongoDB 是一种开源的 NoSQL 数据库,它使用 BSON(类似 JSON 的二进制格式)来存储数据,并提供强大的查询功能。MongoDB 采用文档存储(Document-Oriented)模式,与传统的关系型数据库(RDBMS)不同,MongoDB 不使用表和行,而是以 文档(Document)和集合(Collection) 组织数据。
2. MongoDB 的核心特点有哪些?
- 灵活的数据模型:无需预定义模式(Schema-less)。
- 高效的查询能力:支持丰富的查询语法,包括索引、聚合查询等。
- 水平扩展(Sharding):支持自动分片,实现大数据存储和高并发处理。
- 高可用性(Replication):支持副本集机制,增强数据可用性和故障恢复能力。
- 性能优越:数据以 BSON 格式存储,读写速度快,支持缓存。
数据模型和基本操作
3. MongoDB 中 Collection 和 Document 有什么区别?
- Collection(集合):类似于关系型数据库中的表(Table),但集合是模式无关的,可以存储不同结构的文档。
- Document(文档):类似于 SQL 中的行(Row),数据存储格式为 BSON(类似 JSON 的格式)。
示例:
// 一个文档
{
"_id": ObjectId("60b8d295f3e3c5e15c3b0d5b"),
"name": "张三",
"age": 28,
"skills": ["Java", "MongoDB", "Node.js"]
}
// 整个集合可能包含:
[
{ "_id": ObjectId("1"), "name": "张三", "age": 28 },
{ "_id": ObjectId("2"), "name": "李四", "age": 30 }
]
4. 如何在 MongoDB 中创建数据库?
use myDatabase
如果 myDatabase 不存在,MongoDB 会在插入数据后自动创建它。
5. MongoDB 如何插入数据?
db.users.insertOne({
"name": "李四",
"age": 30,
"skills": ["Python", "MongoDB"]
});
db.users.insertMany([
{ "name": "王五", "age": 25 },
{ "name": "赵六", "age": 35 }
]);
查询与索引
6. MongoDB 的基本查询操作?
// 查询所有文档
db.users.find()
// 查询 age 为 30 的用户
db.users.find({ "age": 30 })
// 只返回 name 字段,隐藏 _id
db.users.find({ "age": 30 }, { "name": 1, "_id": 0 })
7. 如何使用比较运算符($gt, $lt, $gte, $lte)进行查询?
// 查询 age 大于 30 的用户
db.users.find({ "age": { "$gt": 30 } })
// 查询 age 介于 25 到 35 之间的用户
db.users.find({ "age": { "$gte": 25, "$lte": 35 } })
8. MongoDB 中索引的作用是什么?如何创建索引?
索引可以提高查询的速度,MongoDB 默认会在 _id 字段上创建索引。
- 创建索引:
db.users.createIndex({ "name": 1 }) // 1 表示升序索引
db.users.createIndex({ "age": -1 }) // -1 表示降序索引
- 查看索引:
db.users.getIndexes()
- 删除索引:
db.users.dropIndex("name_1")
数据更新和删除
9. MongoDB 如何更新文档?
// 更新单个文档
db.users.updateOne(
{ "name": "张三" },
{ "$set": { "age": 31 } }
)
// 更新多个文档
db.users.updateMany(
{ "age": { "$lt": 30 } },
{ "$set": { "status": "young" } }
)
10. MongoDB 如何删除文档?
// 删除单个文档
db.users.deleteOne({ "name": "李四" })
// 删除多个文档
db.users.deleteMany({ "age": { "$gt": 35 } })
高级功能
11. 什么是 Replication?MongoDB 如何实现高可用性?
Replication(副本集)是一种 MongoDB 的高可用性机制。通过创建多个数据库 副本(Primary、Secondary),MongoDB 实现了读写分离,并可在 Primary 服务器出现故障时自动切换到备份服务器。
- Primary(主节点):负责处理所有写入和读取请求。
- Secondary(从节点):从主节点同步数据,并在主节点故障时接管。
启用副本集:
mongod --replSet "rs0"
初始化副本:
rs.initiate()
12. 如何实现 MongoDB 水平扩展?
MongoDB 通过 Sharding(分片) 技术实现水平扩展。数据可以分布在多个服务器上,适用于大规模数据存储和高并发场景。
Sharding 关键组件:
- Shard(数据分片):负责存储数据
- Config Server(配置服务器):管理数据分布信息
- Mongos(路由管理):负责接收客户端请求,分发到合适的 Shard 服务器
创建分片:
sh.enableSharding("myDatabase")
sh.shardCollection("myDatabase.users", { "age": "hashed" })
事务处理
13. MongoDB 4.0 如何支持事务?
MongoDB 4.0 及以上版本支持 多文档事务(Multi-Document Transactions),类似 SQL 事务,确保多个操作作为一个整体执行。
示例:
const session = db.getMongo().startSession();
session.startTransaction();
try {
session.getDatabase("myDatabase").users.updateOne(
{ "name": "张三" },
{ "$inc": { "balance": -100 } }
);
session.getDatabase("myDatabase").users.updateOne(
{ "name": "李四" },
{ "$inc": { "balance": 100 } }
);
session.commitTransaction();
} catch (error) {
session.abortTransaction();
}
session.endSession();
性能优化
14. MongoDB 如何优化查询性能?
优化 MongoDB 性能的方法:
- 使用索引:创建适当索引,加速查询速度
- 使用投影(Projection):避免返回不必要的字段
- 批量操作:减少请求次数,提高吞吐量
- 使用 Aggregation 代替 find() + filter 组合
- 使用分片(Sharding)来水平扩展
15. 如何监控和分析 MongoDB 性能?
explain("executionStats")查看查询性能:
db.users.find({ "age": 30 }).explain("executionStats")
mongostat和mongotop监测数据库状态:
mongostat
mongotop
结语
掌握 MongoDB 的基本操作、索引、事务、分片等功能,对于面试非常重要。希望这些 MongoDB 面试题和答案能够帮助你顺利通过面试!🚀如果有更深入的问题,欢迎交流!

浙公网安备 33010602011771号