尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JVM与MySQL面试实战:类加载、三色标记与索引锁机制

JVM与MySQL面试实战:类加载、三色标记与索引锁机制 这次我们聊一套能真正用到面试和线上排查里的 JVM MySQL 知识主线。内容不是把 45 道题一个个背下来而是把 JVM 的类加载机制、运行时数据区、垃圾回收原理、三色标记、性能调优工具以及 MySQL 的索引、事务、锁、日志这些高频考点串成一条清晰的主线。如果你正在准备 Java 后端面试或者已经在线上被 OOM、GC 停顿、慢 SQL、死锁这类问题折磨过这篇内容可以收藏起来反复看。先给结论这套东西不需要 GPU不需要特殊硬件只要本机有 JDK 8 或 JDK 11/17、一个 MySQL 客户端就能边学边验证。文章会按“原理讲解 → 工具操作 → 真实题干分析 → 排查方法”的顺序展开。JVM 部分重点讲类加载的五个阶段、双亲委派、内存模型、GC Roots、三色标记和 G1/ZGCMySQL 部分重点讲 B 树索引、事务隔离级别、MVCC、行锁与间隙锁、redo log/binlog、慢查询优化。每一节都会附带可以直接复制运行的命令或代码示例看完能立刻在本机实验。1. 核心能力速览能力项说明内容范围JVM 类加载、内存模型、GC 原理、三色标记、垃圾回收器、性能调优工具、MySQL 索引/事务/锁/日志适合人群Java 后端开发、准备 JVM 面试和 MySQL 面试的候选人、需要做线上问题排查的工程师前置要求熟悉 Java 基础语法了解简单的 SQL运行环境JDK 8 或 JDK 11/17推荐用 JDK 11 测试 G1JDK 17 可体验 ZGC是否需要 GPU不需要纯 CPU 即可是否需要安装服务本机需要 JDKMySQL 可以本地安装也可以直接用 Docker 拉镜像是否支持批量任务JVM 数据采集和 MySQL 慢查询分析可以通过 Shell/Python 脚本批量执行验证难度低命令和代码片段即可验证核心知识点面试侧重既要能讲出原理也要能结合线上现象给出排查路径这套东西最大的价值在于面试题背后其实是同一套排查能力。比如“JVM 内存模型”不是让你背堆和栈的定义而是让你看到 OOM 日志时能判断是哪块区域出了问题“三色标记”不是纯理论而是 CMS 和 G1 并发标记阶段的设计基础“MySQL 事务隔离级别”不只是概念它直接决定了你排查死锁和幻读时该看哪个日志。2. JVM 类加载机制核心考点类加载是 JVM 面试的第一道高频题因为它牵涉到 JVM 的整体架构、双亲委派模型、热部署和模块化。面试官通常从“一个 Java 类从编写到被 JVM 使用经历了哪些阶段”问起。2.1 类加载的五个阶段一个类从被 JVM 加载到可以被使用经历加载、验证、准备、解析、初始化五个阶段。其中加载、验证、准备、解析这四个阶段是 JVM 自动完成的初始化阶段才真正执行类构造器clinit()方法。加载阶段负责找到类的字节码文件通过类的全限定名获取二进制字节流然后把字节流转换为方法区中的运行时数据结构并在堆中生成一个代表这个类的java.lang.Class对象作为访问该类的入口。验证阶段是 JVM 做的安全校验包括文件格式验证、元数据验证、字节码验证、符号引用验证。这一步是为了防止恶意字节码破坏 JVM 运行环境。准备阶段会为类的静态变量分配内存并设置初始值。注意这里设置的是零值比如static int count 100在准备阶段count的值是 0真正的 100 要在初始化阶段才赋值。解析阶段把常量池中的符号引用替换为直接引用。比如类里引用其他类的方法在编译期只是一个符号解析阶段会把它指向实际的内存地址。初始化阶段执行clinit()方法这个方法由静态变量赋值语句和静态代码块合并生成。子类初始化之前会先触发父类初始化JVM 会保证clinit()在多线程环境下被正确加锁同步所以如果静态代码块里有耗时操作会影响整个类的首次加载速度。2.2 类加载器与双亲委派机制JVM 内置了三层类加载器。Bootstrap ClassLoader 由 C 实现负责加载JAVA_HOME/lib目录下的核心类库比如rt.jar、java.lang包它不继承java.lang.ClassLoader。JDK 9 模块化之后扩展类加载器改成了平台类加载器 PlatformClassLoader负责加载 JDK 模块中的类。应用类加载器 AppClassLoader 负责加载 classpath 下的类也是我们平时写代码默认使用的类加载器。双亲委派机制指的是一个类加载器收到类加载请求时先把请求委派给父类加载器父类加载器处理不了才由子类加载器自己加载。protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先检查类是否已加载 Class? c findLoadedClass(name); if (c null) { try { if (parent ! null) { c parent.loadClass(name, false); } else { c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父类找不到由子类自己加载 } if (c null) { c findClass(name); } } return c; } }双亲委派的核心作用是避免类被重复加载以及保证核心类库的安全。比如你写一个java.lang.String由于双亲委派加载请求会被委派给 Bootstrap ClassLoader它发现JAVA_HOME/lib下已经有java.lang.String就不会加载你自定义的那个防止核心类被篡改。面试常追问的问题包括能不能自己写一个java.lang.String并成功加载答案是不能因为双亲委派会拦截即使你通过自定义类加载器强行加载也会在defineClass阶段因为包名以java.开头抛出SecurityException。什么场景需要破坏双亲委派典型场景包括 JDBC 的 SPI 机制、Tomcat 的 Web 应用隔离、热部署框架。JDBC 中DriverManager是由 Bootstrap 加载的但具体的数据库驱动实现类在 classpath 下Bootstrap 无法加载所以ServiceLoader通过线程上下文类加载器突破双亲委派。怎么实现热部署版本号不同、路径不同的类可以靠多个自定义类加载器加载替换类时把旧的类加载器丢掉用新的类加载器重新加载。2.3 自定义类加载器实验理解类加载机制最好的方式是自己写一个类加载器。下面的例子演示了如何从指定目录加载 class 文件import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath classPath; } Override protected Class? findClass(String name) throws ClassNotFoundException { String fileName classPath File.separator name.replace(., File.separator) .class; try (FileInputStream fis new FileInputStream(fileName)) { byte[] bytes new byte[fis.available()]; fis.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } }使用方式# 先编译 javac com/demo/User.java # 然后用自定义类加载器加载 java -cp . MyClassLoaderTest如果你用自定义类加载器加载一个已经被应用类加载器加载过的同名类JVM 会直接返回已有的 Class 对象不会重新加载。这就是类加载器的隔离与缓存机制。面试时能把这一层讲清楚基本就能证明你对类加载有实战理解。3. JVM 运行时数据区与对象分配JVM 内存模型是面试必考的第二块内容。很多候选人能背出堆、栈、方法区但一遇到实际报错就分不清是StackOverflowError还是OutOfMemoryError: Java heap space。这里把运行时数据区拆开讲。3.1 各区域的作用与异常类型程序计数器是一块很小的内存区域记录当前线程执行的字节码行号。它是唯一不会发生OutOfMemoryError的区域。虚拟机栈描述 Java 方法执行的线程内存模型每个方法调用对应一个栈帧栈帧里包含局部变量表、操作数栈、动态链接、返回值等。栈深度不够时抛出StackOverflowError栈容量动态扩展失败时抛出OutOfMemoryError。本地方法栈服务于 Native 方法HotSpot 虚拟机把本地方法栈和虚拟机栈合并了所以平时排查时不用刻意区分。堆是绝大多数对象分配的区域也是 GC 的主要战场。堆内存不足时抛出OutOfMemoryError: Java heap space。方法区用于存储类型信息、常量、静态变量、JIT 编译后的代码缓存。JDK 8 之前叫做永久代JDK 8 之后改成元空间元空间使用本地内存默认不设上限所以元空间溢出时会看到OutOfMemoryError: Metaspace常见原因是 CGLIB 动态生成类过多或反射使用不当。直接内存不属于运行时数据区但 NIO 的DirectByteBuffer使用频繁也会抛出OutOfMemoryError: Direct buffer memory。3.2 对象创建流程与内存布局JVM 创建一个普通 Java 对象的流程如下检查类是否已被加载如果没有先触发类加载。为对象分配内存分配方式分为指针碰撞和空闲列表两种取决于堆是否规整堆是否规整又取决于垃圾回收器是否带压缩整理功能。把分配到的内存空间初始化为零值这样对象的实例字段在 Java 代码里不用赋初值就能直接使用默认值。设置对象头包括 Mark Word、指向类元数据的指针、数组长度如果是数组对象。执行init方法也就是构造函数按照程序员的意愿初始化对象。对象在堆中的内存布局分为三部分对象头、实例数据、对齐填充。对象头里的 Mark Word 存储了哈希码、GC 分代年龄、锁状态标志等信息这部分内容在并发编程和 GC 调优里非常关键。对象的访问定位方式有两种句柄访问和直接指针访问。HotSpot 默认使用直接指针访问方式好处是访问速度更快不需要多一次指针跳转。3.3 堆内存分配实验下面这段代码可以直观观察堆内存变化public class HeapAllocTest { public static void main(String[] args) throws Exception { System.out.println(total heap: Runtime.getRuntime().totalMemory() / 1024 / 1024 MB); System.out.println(max heap: Runtime.getRuntime().maxMemory() / 1024 / 1024 MB); System.out.println(free heap: Runtime.getRuntime().freeMemory() / 1024 / 1024 MB); byte[][] bytes new byte[1024][]; for (int i 0; i 1024; i) { bytes[i] new byte[1024 * 1024]; Thread.sleep(50); System.out.println(allocate (i 1) MB); } } }运行前设置堆大小java -Xms64m -Xmx128m -XX:PrintGCDetails HeapAllocTest执行后可以清楚看到堆空间增长、GC 触发、java.lang.OutOfMemoryError: Java heap space异常发生的过程。这就是在真实 JVM 里验证内存模型的最快方式。4. GC 原理与垃圾回收机制GC 是 JVM 面试里最核心的部分也是线上问题排查中最常遇到的领域。理解 GC 的关键在于三件事怎么判断对象是垃圾用什么算法回收垃圾选择哪个垃圾回收器来执行回收。4.1 判断对象是否存活引用计数法是最简单的方案给对象维护一个计数器每增加一个引用计数加 1失效就减 1计数为 0 即可回收。但引用计数法无法处理循环引用问题比如 A 引用 B、B 引用 A并且两者都无外部引用计数器永远不为 0。HotSpot 使用的是可达性分析算法。以 GC Roots 为起点向下搜索引用链没有任何引用链可达的对象就判定为可回收对象。需要记住 GC Roots 包括以下几类虚拟机栈中局部变量表引用的对象。本地方法栈中 JNI 引用的对象。方法区中类的静态属性引用的对象。方法区中常量引用的对象。所有被同步锁 synchronized 持有的对象。Java 虚拟机内部的 Class 对象、系统类加载器等。面试里经常会问“什么样的对象可以当 GC Roots”。回答时要结合具体场景比如栈帧中当前正在执行的局部变量如果一个对象保存在静态字段里它一定不会被回收。Java 的引用类型按强度分为强引用、软引用、弱引用、虚引用。强引用只要可达就不会被回收软引用在内存不足时被回收弱引用在下一次 GC 时就可能被回收虚引用主要用于对象回收监听比如PhantomReference配合ReferenceQueue使用在 DirectByteBuffer 的回收中就有应用。4.2 垃圾回收算法标记-清除算法是最基础的方案分为标记和清除两个阶段效率不太稳定标记和清除两阶段都需要扫描所有对象清除之后产生大量不连续的内存碎片会导致后续大对象无法分配。标记-复制算法把可用内存分成两块每次只使用其中一块回收时把存活对象复制到另一块然后清空当前块。这种算法实现简单、内存紧凑但可用内存缩小了一半。现代商用虚拟机把新生代分成 Eden 区和两个 Survivor 区比例一般是 8:1:1也就是把复制算法优化成了只浪费 10% 的 Survivor 区空间。标记-整理算法在标记之后让存活对象向一端移动直接清理掉边界以外的内存。这种算法没有内存碎片但移动对象需要更新所有引用地址停顿时间会更长。老年代一般使用标记-整理算法或 CMS 配合碎片整理方案。4.3 分代收集与常见垃圾回收器分代收集并不是某一种算法而是根据对象存活周期把堆分成新生代和老年代。新生代对象“朝生夕灭”适合复制算法老年代对象存活率高适合标记-整理或标记-清除算法。常见回收器的适用版本和特点回收器适用区域特点备注Serial新生代单线程、简单高效Client 模式默认ParNew新生代Serial 的多线程版本常与 CMS 配合Parallel Scavenge新生代关注吞吐量JDK 8 默认组合Serial Old老年代Serial 的老年代版本与 CMS 组合备用Parallel Old老年代吞吐量优先与 Parallel Scavenge 组合CMS老年代并发标记清除、低停顿JDK 9 起废弃G1整堆Region 化、可预测停顿JDK 9 起默认ZGC整堆超低停顿、着色指针适合大堆场景JDK 8 默认使用 Parallel Scavenge Parallel OldJDK 9 起默认使用 G1JDK 17 中 ZGC 已经可以稳定使用。面试中如果提到“默认回收器”一定要说清楚 JDK 版本。CMS 高频考点是它的七个阶段和两个问题并发标记阶段用户线程和 GC 线程并发执行会产生新对象由于 CMS 是标记-清除算法会产生内存碎片当老年代空间不足以分配对象时会触发 Foreground 模式的 Full GC这时的停顿可能非常长。G1 的出现就是为了同时解决碎片化和停顿可预测的问题。5. 三色标记算法详解三色标记是并发标记阶段的核心理论也是 CMS 和 G1 面试题里最容易翻车的点。很多候选人知道颜色标记但说不清“错标漏标”和解决方案。5.1 三种颜色状态在可达性分析过程中对象被标记成三种颜色白色还没被访问到的对象。初始阶段所有对象都是白色如果标记结束后仍然是白色说明不可达可以被回收。灰色当前对象被访问到了但它引用的其他对象还没被扫描过。灰色对象是标记工作的中间状态。黑色当前对象和它引用的所有对象都已经被扫描过不会再参与后续扫描。标记过程就是从 GC Roots 开始把可达对象从白色变成灰色再逐层扫描灰色对象把它引用的对象标记成灰色自己从灰色变成黑色。最终所有黑色对象是存活对象白色对象是垃圾。5.2 三色标记的漏标问题并发标记的最大风险是漏标一个原本应该存活的对象被当作白色垃圾回收了。要发生漏标必须同时满足两个条件一个黑色对象指向了某个白色对象。所有指向该白色对象的引用都被移除。为什么黑色对象指向白色对象就有问题因为黑色对象已经被视为“扫描完成”不会再被当作扫描起点去遍历它的引用。如果它指向了白色对象这个白色对象就永远不会被扫描到。此时如果其他路径对它的引用也被切断它就会在并发标记结束后被判为垃圾。5.3 CMS 的增量更新与 G1 的原始快照为了解决漏标问题CMS 和 G1 采用了不同方案。CMS 使用增量更新Incremental Update当黑色对象被插入一条指向白色对象的新引用时记录这个引用并发标记结束后以这个黑色对象为根重新扫描一次。增量更新的核心思路是“记录新增引用防止黑色对象漏掉新指向的白色对象”。G1 使用原始快照SATBSnapshot At The Beginning在并发标记开始时保存一份对象引用关系的快照。当某个指向白色对象的引用被删除时记录这个被删除的引用标记结束后以被删除引用的起始对象为根再扫描。SATB 的核心思路是“记录引用删除保证标记开始时已经存在的对象不被漏掉”。面试中再配一个 G1 的细节G1 的写屏障记录的是引用变化前后的旧值快照配合 SATB 保证并发标记的正确性。增量更新和 SATB 不是互斥的两种方案都需要牺牲一部分吞吐量来保证正确性。6. JVM 性能调优与问题排查JVM 调优不是玄学也不是把网上的 JVM 启动参数抄一遍。调优的前提是先拿到现场数据再根据数据决定调整哪个参数。下面介绍一套最常用的排查流程和命令。6.1 常用 JVM 工具命令JDK 自带工具是排查问题最可靠的方式常见工具有这些命令作用jps列出当前 JVM 进程和启动类jstat查看类加载、GC、JIT 编译统计jmap生成堆转储快照 dump 文件jstack生成线程快照查看线程状态和死锁jcmd整合了多种 JVM 诊断命令jinfo查看和调整 JVM 参数先查看进程jps -l查看 GC 统计jstat -gc pid 1000 10这条命令会每 1 秒输出一次 GC 统计持续 10 次分别显示 S0C、S1C、EC、OC、MC 等区域大小、YGC 次数、FGC 次数和 GC 耗时。生成堆转储文件jmap -dump:live,formatb,file/tmp/heap.hprof pid查看线程状态重点看是否有 BLOCKED、DEADLOCK、WAITING 状态jstack pid如果堆内存紧张还可以主动触发一次 GCjcmd pid GC.run6.2 从堆转储分析 OOM 问题拿到 heap.hprof 文件后可以用 JDK 自带的 VisualVM 或 MAT 分析。分析步骤是打开 dump 文件查看 Leak Suspects定位可能泄漏的对象。查看 Dominator Tree找出占用堆内存最大的对象。查看 GC Roots 引用链确定对象是被谁持有的。如果是 Array 对象占用过高优先看是不是大 List、大缓存没有清理如果是 byte[] 占用过高优先检查 IO 流、文件流是否关闭或有没有大量缓存图片。如果是 Thread 对象过多检查线程池是否没有回收比如 new Thread 没有 join、线程池提交任务没 set daemon。一个典型的 OOM 排查流程是# 1. 找到 Java 进程 jps -l # 2. 观察 GC 和堆变化 jstat -gcutil pid 1000 # 3. 生成堆 dump jmap -dump:live,formatb,file/tmp/oom.hprof pid # 4. 保存线程快照 jstack pid /tmp/thread.txt # 5. 用 MAT 分析堆 dump # org.eclipse.mat.ui: 打开 /tmp/oom.hprof这里也要提醒一点如果服务已经 OOM 崩溃堆 dump 可能来不及。可以提前加 JVM 启动参数让 JVM 在 OOM 时自动输出堆 dump-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/oom.hprof -XX:ExitOnOutOfMemoryError6.3 线上常见 JVM 启动问题很多同学会遇到启动时报[error] could not get jvm parameters and dynamic configurations properly. [e...这种问题通常是启动脚本中 JVM 参数格式写错或者 JVM 版本与配置不匹配。建议先把启动脚本里的-Xms、-Xmx、-XX:参数逐个拆开检查再用java -version确认当前 JDK 版本。还有一类问题是开发工具里找不到 JVM。Eclipse 启动 Tomcat 时报eclipse 找不到或无法加载主类 org.apache.catalina.startup.bootstrap或者命令行报no jvm could be found on your system多半是JAVA_HOME环境变量没配好或者 Eclipse 的 Installed JREs 指向了不存在的 JDK 路径。解决方式是把JAVA_HOME明确指向 JDK 根目录并确认%JAVA_HOME%\bin在PATH里。Gradle 项目也会遇到the projects gradle version 6.7.1 is incompatible with the gradle jvm version这类报错原因是 Gradle 使用的 JVM 版本过高或过低需要检查gradle/wrapper/gradle-wrapper.properties里的 Gradle 版本以及 IDEA 中 Project SDK 和 Gradle JVM 是否一致。JDK 8 项目硬要跑在 JDK 17 下很容易出现这类兼容性问题。6.4 Docker 容器中 JVM 日志位置线上很多 Java 服务跑在 Docker 容器里日志位置和常规部署不太一样。常见排查方式是先进入容器docker ps | grep service-name docker logs container-id --tail 200 docker exec -it container-id bash进入容器后如果 Java 进程还在可以用 jps 查看。容器里通常没有完整 JDK只有 JRE可能没有 jmap但jcmd或jstack可能可用。如果出现频繁重启检查启动脚本里的HeapDumpOnOutOfMemoryError是否生效dump 文件是否挂载到宿主机。日志挂载一般通过-v /data/logs:/logs完成容器崩溃前打印到 stdout 的日志会通过docker logs看到。7. MySQL 面试高频考点MySQL 是后端面试里和 JVM 并列的核心主题。常见问题包括存储引擎、索引结构、事务隔离级别、MVCC、锁机制、redo log/binlog、慢查询优化。下面按主线全部过一遍。7.1 存储引擎InnoDB 与 MyISAM面试第一问通常是“InnoDB 和 MyISAM 的区别”。回答关键点对比项InnoDBMyISAM事务支持不支持外键支持不支持锁粒度行锁表锁崩溃恢复redo log 支持不支持全文索引支持8.0 内置支持聚簇索引有无MySQL 8.0 之后默认存储引擎是 InnoDB这一点必须说清楚。另一个高频点只有主键索引是聚簇索引二级索引叶子节点存的是主键值所以用二级索引查询时可能会发生回表。7.2 B Tree 索引索引是 MySQL 面试的重灾区。核心要点是InnoDB 的索引数据结构是 B Tree而不是 B Tree 或二叉树。B Tree 的特点所有数据都存放在叶子节点。叶子节点之间通过链表连接适合范围查询。非叶子节点只存放键值树的高度低减少磁盘 IO。同一层节点之间没有指针数据按索引顺序排列。为什么不用哈希索引哈希索引适合等值查询但无法支持范围查询和排序所以 InnoDB 默认使用 B Tree。为什么不用红黑树红黑树是二叉搜索树树的高度比 B Tree 高得多数据量大时磁盘 IO 次数过多。B Tree 三层通常就能支撑千万级数据红黑树则需要更多层。必须记住的索引失效场景对索引列使用函数或计算比如where id 1 10无法命中id索引。隐式类型转换比如where mobile 13812341234mobile 是 varchar数字会被转换成字符串比较也可能导致索引失效。这对应网上的经典题干mysql中int5本质是列上做运算导致索引失效。左模糊查询like %abc无法使用 B Tree 索引。联合索引不满足最左前缀原则。联合索引有一个高频追问(a, b, c)联合索引where a 1 and c 3能走索引吗答案是可以走索引但只能用到ac无法利用联合索引的第二个字段因为中间跳过了b。优化方式是调整索引顺序让最常过滤的字段在最前面。7.3 事务隔离级别与 MVCC事务的 ACID 属性里“隔离性”是面试重点。四种隔离级别隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交不可能可能可能可重复读不可能不可能可能InnoDB 在 RR 下使用 MVCC 间隙锁解决大部分幻读串行化不可能不可能不可能MySQL 默认隔离级别是可重复读这也是面试最容易问到的点。为什么 MySQL 选择 RR 而不是 RC因为在主从复制场景下RC 隔离级别无法保证日志在 binlog 中按事务提交顺序正确回放RR 配合间隙锁可以避免部分幻读问题。MVCC 的实现依赖三个结构隐藏字段、undo log、ReadView。隐藏字段包括DB_TRX_ID最近修改该行的事务 ID和DB_ROLL_PTR指向 undo log 版本链的指针。ReadView 包含m_ids活跃事务 ID 列表、min_trx_id最小活跃事务 ID、max_trx_id下一个事务 ID、creator_trx_id创建这个 ReadView 的事务 ID。查询时根据当前事务 ID 与版本链上事务 ID 的可见性关系决定读取哪个版本。RC 和 RR 的区别在于生成 ReadView 的时机不同。RC 在每次读操作时都生成新的 ReadView所以能读到其他事务已提交的数据RR 只在第一次读操作时生成 ReadView整个事务期间复用同一个 ReadView所以保证了可重复读。7.4 锁机制MySQL 锁的层次包括全局锁、表锁、行锁、间隙锁、临键锁。面试重点在 InnoDB 的行锁和间隙锁。记录锁锁住的是索引记录本身间隙锁锁住的是索引记录之间的区间临键锁是记录锁 间隙锁的组合。默认 RR 隔离级别下InnoDB 使用临键锁解决幻读问题。死锁的高频题干是事务 A 更新 id1 再更新 id2事务 B 更新 id2 再更新 id1两个事务互相等待。排查方式SHOW ENGINE INNODB STATUS\G在输出中查找LATEST DETECTED DEADLOCK部分可以看到事务持有什么锁、正在等待什么锁从而确定加锁顺序修改业务代码保证所有事务按相同顺序加锁。7.5 redo log、undo log、binlogredo log 是 InnoDB 存储引擎层的日志主要用来崩溃恢复。数据修改先写 redo log再刷盘这个过程叫 WALWrite-Ahead Logging。如果数据库在事务提交后、数据落盘前崩溃重启时通过 redo log 恢复已提交事务的数据。undo log 用于事务回滚和 MVCC。修改数据前生成 undo log记录数据旧版本回滚时通过 undo log 恢复旧值。binlog 是 MySQL Server 层的日志用于主从复制和数据归档。redo log 是物理日志记录“某个页做了哪些修改”binlog 是逻辑日志记录 SQL 语句或行变更。MySQL 为了保证 redo log 和 binlog 一致使用了两阶段提交协议。先写 redo logprepare 状态再写 binlog最后提交事务并把 redo log 改成 commit 状态。如果 binlog 写了一半宕机重启后会回滚未完成的 XA 事务避免两个日志不一致导致主从数据不一致。7.6 explain 与慢查询优化SQL 性能优化是 MySQL 面试的实操环节。定位慢 SQL 后第一步就是看执行计划EXPLAIN SELECT * FROM user WHERE mobile 13800138000;需要重点关注的字段是type至少看到const、eq_ref、ref、range如果出现ALL说明全表扫描。key实际使用到的索引。rows预估扫描行数数值越小越好。Extra出现Using filesort或Using temporary时说明排序或分组没有利用到索引需要优化。典型优化案例-- 慢查询案例手机号和状态字段分别建索引但查询使用了函数 SELECT * FROM order_table WHERE DATE(create_time) 2024-06-01; -- 优化避免对索引列使用函数 SELECT * FROM order_table WHERE create_time 2024-06-01 00:00:00 AND create_time 2024-06-02 00:00:00;再比如where order_status in (1,2,3) order by id desc limit 20如果order_status选择性太低优化器可能直接走主键全扫更好的方式是通过业务约束缩小数据范围或者把order_status和create_time组合成联合索引。8. MySQL 实际运维与常见问题MySQL 面试不止于概念也会问安装、巡检、问题排查。下面把常见题干和解决方法汇总一下。8.1 MySQL 安装与启动报错Windows 安装 MySQL 后服务启动报错常见原因有三类my.ini配置的basedir和datadir路径不对或者目录下存在旧数据文件。端口 3306 被其他程序占用。初始化数据库时没有执行mysqld --initialize-insecure导致缺少 system 表或 root 密码。解决步骤# 先停止服务 net stop mysql # 检查端口 netstat -ano | findstr 3306 # 重新初始化数据目录 mysqld --initialize-insecure --basedirD:/mysql --datadirD:/mysql/data # 启动服务 net start mysqlLinux 下容器部署更常见。用 Docker 启动 MySQL 时的示例docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e TZAsia/Shanghai \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/data:/var/lib/mysql \ mysql:8.0如果容器里报firedac phys mysql client does not support authentication protocol requested通常是 MySQL 8 默认使用caching_sha2_password插件而旧版本客户端不支持。解决方式是把账户认证插件改成mysql_native_passwordALTER USER root% IDENTIFIED WITH mysql_native_password BY root123; FLUSH PRIVILEGES;8.2 MySQL Workbench 命令行建表有些同学刚接触 Workbench找不到在哪执行 SQL。可以直接用命令行客户端mysql -uroot -p mysql use test_db; mysql CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, mobile VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id), KEY idx_mobile (mobile) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;8.3 唯一索引遇到已有重复数据给已有大量数据的表加唯一索引时如果存在重复数据会直接报错。处理方式是先找出重复数据再删除或合并SELECT mobile, COUNT(*) FROM user GROUP BY mobile HAVING COUNT(*) 1;删除重复数据只保留最小 id 后再添加唯一索引DELETE u1 FROM user u1 INNER JOIN user u2 ON u1.mobile u2.mobile AND u1.id u2.id; ALTER TABLE user ADD UNIQUE KEY uk_mobile (mobile);8.4 MySQL 中常见 SQL 陷阱mysql中int5这个题干考察的是隐式类型转换。如果某列是 int 类型查询时写where id 5 10由于对索引列做了运算索引会失效正确写法是where id 10 - 5。mysql的or能去重吗这个问题很简单or是逻辑或条件不会去重。如果想去重加DISTINCTSELECT DISTINCT name FROM user WHERE status 1 OR status 2;不过在大表场景中or可能导致索引合并的不确定性更稳妥的优化是用UNION或拆分成多条查询。8.5 MySQL 存储过程和排序优化存储过程在日常开发里用得少了但面试中偶尔会问。一个简单的插入测试数据存储过程DELIMITER // CREATE PROCEDURE insert_test_data(IN total INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i total DO INSERT INTO user(name, mobile) VALUES(CONCAT(user, i), CONCAT(138, LPAD(i, 8, 0))); SET i i 1; END WHILE; END // DELIMITER ; CALL insert_test_data(10000);MySQL 排序优化的核心是避免filesort。ORDER BY的字段最好是索引列且与WHERE使用的索引满足最左前缀原则。如果确实需要排序可以在业务侧限制排序结果集大小或者对高频排序字段建联合索引。9. JVM 与 MySQL 训练复盘与最佳实践面试题背下来只是第一步真正能拿高分的候选人都能把知识点串成一条排查链路。JVM 和 MySQL 实际上是一套体系的两面JVM 管的是应用进程的稳定性MySQL 管的是数据存储层的稳定性。9.1 从面试题到真实排查能力建议按下面的路径训练遇到 JVM 内存模型题不要只背概念实际跑一个 OOM 实验用 jmap 生成 dump 文件再用 MAT 打开看对象分布。遇到 GC 题用 jstat 观察一个压力测试程序的 YGC 和 FGC再调小-Xmx复现频繁 Full GC观察停顿。遇到三色标记题配合 CMS 并发标记失败的日志来理解日志里出现concurrent mode failure时说明标记阶段用户线程内存分配压力过大已经触发 Full GC。遇到 MySQL 索引题用EXPLAIN查看不同 SQL 的执行计划验证索引失效原则。遇到死锁题用SHOW ENGINE INNODB STATUS看真实死锁日志而不是只背死锁定义。这套训练逻辑就是“先复现问题再分析数据最后调整参数或 SQL”。面试官问你线上问题排查经验时你可以直接说先看报错日志定位异常类型再用工具采集 GC/线程/堆/慢查询信息最后根据数据决定是调 JVM 参数、优化 SQL还是改代码。9.2 工程化落地建议结合多个线上项目的实际经验给出这样几条建议第一JVM 参数不要盲目照抄。不同业务对堆大小的要求完全不同。一个 2C4G 的容器服务-Xmx设置成 3G 可能没问题但如果里面有大量本地缓存最终内存会超限。调优前用jstat观察一周的 GC 趋势再决定要不要加堆。第二JVM 日志一定要落到磁盘并加上滚动策略。很多线上事故复盘时发现服务重启了但没有任何 GC 日志导致无法确认是不是 OOM。启动参数建议这样加-Xms512m -Xmx512m \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/logs/oom.hprof \ -Xloggc:/data/logs/gc.log \ -XX:PrintGCDetails \ -XX:PrintGCDateStamps \ -XX:PrintHeapAtGCJDK 11 及以后的版本GC 日志参数有变化-Xlog:gc*:file/data/logs/gc.log:time,uptime,level,tags第三批量任务和监控要自动化。线上不可能等 OOM 发生后再手工执行 jmap。可以用脚本定期采集 jstat 数据超过阈值就告警。MySQL 慢查询也可以定时导出慢查询日志到分析表用 SQL 统计高频慢 SQL。第四面试答 MySQL 锁问题时一定要结合实际场景。不要只背“间隙锁解决幻读”要说清楚RR 隔离级别下SELECT ... FOR UPDATE或UPDATE操作会加临键锁锁住的范围包括记录本身和前面的间隙从而阻止其他事务插入该范围内的记录避免幻读。同理SELECT普通读走 MVCC不加锁。9.3 一个综合实验模拟线上 OOM 与排查这里给一个综合性实验覆盖 JVM 和 MySQL 两部分。先在 MySQL 里准备一张订单表插入大量数据用慢查询找出低效 SQL然后在一个 Java 程序里模拟堆内存溢出用工具完成排查。MySQL 侧准备数据CREATE DATABASE IF NOT EXISTS test; USE test; DROP TABLE IF EXISTS order_table; CREATE TABLE order_table ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CALL insert_order_data(100000);模拟慢查询SELECT * FROM order_table WHERE DATE(create_time) 2024-06-01;用EXPLAIN看需要优化的点然后改成范围查询。JVM 侧写一个不断向 List 中添加大对象的小程序设置-Xmx64m -XX:HeapDumpOnOutOfMemoryError等待 OOM 后分析java_pid*.hprof文件。这个实验能一次性覆盖类加载、运行时数据区、GC 日志、OOM 异常、堆 dump 分析、慢 SQL 优化整套流程做完面试题基本就不怕了。10. 总结与下一步这套 JVM MySQL 知识整理最适合三类人一是正在准备 Java 后端面试的候选人二是刚接手线上服务、经常遇到 OOM 和慢 SQL 的工程师三是想系统梳理 JVM 类加载、GC 原理、三色标记和 MySQL 索引锁机制的学习者。你应该先验证的是本机能跑通 jps、jstat、jmap、jstack 这套命令能看懂 GC 日志能通过EXPLAIN判断 SQL 是否走索引。最容易踩的坑是把面试题背得很熟但一进现场看到 GC 日志仍然不知道从哪看起。建议用文中的综合实验从头到尾跑一遍模拟 OOM 和慢查询定位把知识变成肌肉记忆。下一步可以继续深入的方向包括Arthas 在线诊断、JDK Flight Recorder 的使用、G1 日志的完整解读、MySQL InnoDB 底层页结构、分布式事务与两阶段提交。面试题只是入口真实的价值是在问题发生时你能在几分钟内判断出是 JVM 内存泄漏、SQL 索引失效、还是锁竞争导致的系统卡顿。把这套内容刷完至少不会再看到OutOfMemoryError和Deadlock found时手足无措。
返回列表