2025.06.07

1. Java 中操作字符串有哪些类型?它们之间有什么区别?

Java 主要有三种可操作“字符序列”的类型:

  • String

    • 特点:不可变(immutable)、底层基于字符数组 char[]

    • 适用:读多写少的场景,它一旦创建,内容就不可变,保证了线程安全

  • StringBuilder

    • 特点:可变(mutable)、非线程安全,底层同样是 char[],扩容时按 oldCapacity * 2 + 2

    • 适用:单线程下大量拼接字符串的场景,效率比 String + 高,比 StringBuffer

  • StringBuffer

    • 特点:可变、线程安全(方法上用 synchronized),同样基于 char[]

    • 适用:多线程需要频繁修改字符串的场景,但性能低于 StringBuilder

  • CharSequence

    • 说明StringStringBuilderStringBuffer 等都实现了它,表示一段可读的字符序列


2. String 类的常用方法及其作用

  • 长度与内容

    • int length():返回字符串长度(字符个数)。

    • boolean isEmpty():判断字符串是否为空(length() == 0)。

    • char charAt(int index):返回指定下标处的字符,0-based。

    • char[] toCharArray():将字符串转换为 char[]

    • byte[] getBytes():按平台默认编码将字符串转换成字节数组。

  • 比较与查找

    • boolean equals(Object):区分大小写比较内容是否相同。

    • boolean equalsIgnoreCase(String):忽略大小写比较内容是否相同。

    • int compareTo(String another):按字典序比较,返回负/零/正。

    • int indexOf(String/char):查找某子串或字符第一次出现的位置,找不到返回 -1

    • int lastIndexOf(String/char):查找子串或字符最后一次出现的位置,找不到返回 -1

    • boolean contains(CharSequence):判断是否包含指定子串。

    • boolean startsWith(String):判断是否以指定前缀开始。

    • boolean endsWith(String):判断是否以指定后缀结束。

  • 截取与分割

    • String substring(int beginIndex):截取从 beginIndex 到末尾的子串。

    • String substring(int beginIndex, int endIndex):截取从 beginIndex(含)到 endIndex(不含)的子串。

    • String[] split(String regex):按正则表达式拆分,返回字符串数组。

    • String trim():去除两端空白字符(space、tab、换行等),不影响中间空格。

  • 替换

    • String replace(char oldChar, char newChar):全局替换字符。

    • String replace(CharSequence target, CharSequence replacement):全局替换字符串(不支持正则)。

    • String replaceAll(String regex, String replacement):按正则替换,所有匹配均被替换。

    • String replaceFirst(String regex, String replacement):按正则替换,仅替换第一个匹配。

  • 大小写 & 格式化

    • String toLowerCase():转为全小写。

    • String toUpperCase():转为全大写。

    • String format(String fmt, Object… args):按格式化字符串生成新字符串(类似 printf)。

  • 连接与常量池

    • static String join(CharSequence delimiter, CharSequence… elements):将多个序列用 delimiter 连接成一个字符串。

    • String intern():将字符串加入常量池并返回池中引用;若已存在则返回已有引用,便于节省内存和快速比较。

  • 其它

    • boolean matches(String regex):判断整个字符串是否匹配给定正则。

    • 拼接建议:大量拼接时推荐使用 StringBuilderStringBuffer,最后调用 toString() 得到新 String


3. List、Set、Map 之间的区别是什么?

  • List

    • 特点:有序、允许重复

    • 常用实现ArrayListLinkedList

    • 适用:需要按下标访问、允许重复元素的场景

  • Set

    • 特点:无序(或排序)、不允许重复

    • 常用实现HashSetLinkedHashSetTreeSet

    • 适用:需要去重的场景

  • Map

    • 特点:键值对结构,key 不可重复,value 可重复

    • 常用实现HashMapLinkedHashMapTreeMap

    • 适用:需要快速根据 key 查找 value 的场景


4. HashMap 的实现原理

  • 底层结构:数组 + 链表/红黑树(当单个桶中元素过多时,链表转为红黑树以优化性能)

  • 哈希定位hash(key) → 扰动后与 (capacity - 1) 做位与,定位到数组下标

  • 插入

    • 若目标桶为空,直接插入

    • 否则遍历链表/树,若 key 相同则覆盖 value,否则追加到末尾/树中

  • 扩容:当元素数超过 loadFactor(默认 0.75)时,容量翻倍,并重新 rehash 所有元素

  • 查询:同哈希定位过程,遍历桶内链表/树,通过 equals 比较 key,找到则返回对应 value


5. ArrayList 和 LinkedList 的区别

  • ArrayList

    • 底层结构:动态数组 Object[]

    • 随机访问:O(1)

    • 任意位置插入/删除:O(n)(需要移动元素)

    • 尾部插入/删除:均摊 O(1)

    • 适用:读多写少、需要频繁随机访问的场景

  • LinkedList

    • 底层结构:双向链表

    • 随机访问:O(n)

    • 任意位置插入/删除:O(1)(只改动指针)

    • 尾部操作:O(1)

    • 适用:插入/删除操作频繁的场景


6. 创建线程池的几种方式

  • Executors 工厂方法

    • Executors.newFixedThreadPool(int n)

    • Executors.newCachedThreadPool()

    • Executors.newSingleThreadExecutor()

    • Executors.newScheduledThreadPool(int corePoolSize)

  • ThreadPoolExecutor 构造

    new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(),
        threadFactory,
        rejectedExecutionHandler
    );
    

  • ForkJoinPool

    • 适合分治任务,内部使用工作窃取算法

  • Spring 注解方式

    • @EnableAsync + 自定义 TaskExecutor

    • @Scheduled 定时任务线程池


7. 什么是死锁?怎么防止死锁?

  • 死锁:两个或多个线程各自持有对方需要的资源并相互等待,导致都无法继续执行的状态

  • 防止策略

    • 固定顺序加锁:所有线程按同一顺序请求锁,避免循环等待

    • 尝试加锁(tryLock):使用带超时的 tryLock(timeout),超时释放已持有锁再重试

    • 锁分离/细化:缩短持锁时间,减少锁竞争

    • 死锁检测与资源剥夺(复杂,少用)


  • Cookie

    • 存储位置:客户端(浏览器)

    • 容量:≈4KB

    • 生命周期:可设置过期时间

    • 安全性:易被篡改

  • Session

    • 存储位置:服务器

    • 容量:受服务器内存/存储限制

    • 生命周期:通常浏览器关闭或超时后失效

    • 安全性:只在客户端保存 sessionId,相对更安全

  • Session 工作原理

    1. 客户端首次请求时,服务器创建 HttpSession 并生成唯一 sessionId

    2. 服务器通过 Set-Cookie: JSESSIONID=… 下发给客户端

    3. 后续请求浏览器携带该 Cookie,服务器根据 sessionId 找到对应 HttpSession

    4. HttpSession 中存取用户信息、购物车等会话数据


9. 什么是 Spring Boot?Spring Boot 的优点

  • Spring Boot:基于 Spring 框架的快速开发平台,通过“约定大于配置”和自动化配置简化 Spring 应用搭建

  • 优点

    • 开箱即用:内嵌 Tomcat/Jetty,无需外部容器

    • 自动配置:根据类路径依赖自动装配常用组件

    • Starter POMs:一组 Starter 简化依赖管理

    • 生产级监控:Actuator 提供健康检查、指标等

    • 极简配置:application.propertiesyml 一处配置即可


10. Spring Cloud 的核心组件有哪些?

  • Spring Cloud Config(配置中心)

  • Eureka / Consul / Zookeeper(服务注册与发现)

  • Ribbon(客户端负载均衡)

  • Feign(声明式 HTTP 客户端,集成 Ribbon)

  • Hystrix / Resilience4j(断路器)

  • Zuul / Spring Cloud Gateway(API 网关)

  • Bus(事件总线,用于广播配置变更)

  • Sleuth + Zipkin(分布式链路跟踪)

  • Stream(消息驱动微服务)


11. MyBatis 的一级缓存和二级缓存

  • 一级缓存

    • 作用域:SqlSession 级别,同会话中相同语句与参数直接从缓存取

    • 默认开启;commit/rollback/clearCache 会清空

  • 二级缓存

    • 作用域:Mapper(namespace)级别,跨 SqlSession 共享

    • 默认关闭,需要在全局与单个 mapper 中配置 <setting name="cacheEnabled" value="true"/><cache/>,且返回对象须可序列化


12. 自增表中删除后重启再插入,ID 会是多少?

  • InnoDB(默认):删除 ID 6、7 后剩 1~5,重启时自增值 = MAX(id)+1 = 6,插入的新记录 ID 为 6

    • 重启后:InnoDB 会根据表中现有的最大 id(此例为 5)来重新计算下一个自增值,变成 5 + 1 = 6

    • 未重启:自增计数器仍在内存里,继续累加,所以看到的是 8。

  • MyISAM:自增计数器保存在磁盘,重启不变,新记录 ID 为 8


13. ACID 是什么?

  • A (Atomicity,原子性):事务要么全部成功,要么全部失败(回滚)

  • C (Consistency,一致性):事务执行前后保持数据完整性约束

  • I (Isolation,隔离性):并发事务互不干扰,可设不同隔离级别

  • D (Durability,持久性):事务一旦提交,对数据的修改永久保存


14. MySQL 索引是怎么实现的?

  • B+ 树索引:最常用,非叶子节点存 key 和子节点指针;叶子节点存完整 key + 指向数据行的指针(聚簇索引直接存数据行)

  • Hash 索引(MEMORY 引擎):通过哈希表实现,等值查询快,范围查询和排序不支持

  • 全文索引:基于倒排索引,支持 MATCH ... AGAINST

  • R-Tree 索引(MyISAM 空间索引):用于地理空间数据


15. JVM 运行时数据区

  • 程序计数器 (PC Register):存放当前线程所执行字节码的行号指示器

  • Java 栈 (Stack):每个方法调用时创建的栈帧,存储局部变量、操作数栈、动态链接、方法出口等

  • 本地方法栈 (Native Stack):为执行本地方法服务

  • 堆 (Heap):存放所有对象实例和数组,是 GC 管理的重点区域

  • 方法区 (Method Area):存放类信息、常量、静态变量、即时编译器编译后的代码等(HotSpot 中的 Metaspace)

  • 运行时常量池 (Runtime Constant Pool):方法区的一部分,存放编译期产生的各种字面量和符号引用

  • 直接内存 (Direct Memory):不属于 JVM 内存一部分,通过 NIO 等方式分配,用于高效 I/O

16.服务器被入侵了怎么办?

  • 立即断网隔离

  • 保存证据和日志

  • 修改所有密码(包括数据库、应用等)

  • 评估影响范围

  • 备份核心数据

  • 完全重置系统

  • 重装后及时更新系统补丁

  • 从可信备份恢复数据

  • 加强安全配置和监控

  • 编写事件报告

2025.06.11

Vue 2 的生命周期

Vue 2 中,每个组件实例在其生命过程中会经历四大阶段(创建 ▶ 挂载 ▶ 更新 ▶ 销毁),并在各阶段触发相应的生命周期钩子。

1. 创建阶段
  • beforeCreate:实例刚被创建,datamethods 等都尚未初始化,此时不能访问响应式数据或 DOM。

  • created:实例已完成数据观测、属性初始化,可在此时发起异步请求或初始化数据,但仍不可操作真实 DOM。

2. 挂载阶段
  • beforeMount:在挂载开始前被调用,此时模板已编译但尚未插入到页面。

  • mounted:实例已挂载到页面,可在此访问和操作 DOM,例如获取 ref 引用。

3. 更新阶段
  • beforeUpdate:数据更新后、DOM 重新渲染前被调用,可在此检视即将更新的状态。

  • updated:组件 DOM 及数据同步完成后被调用,谨慎避免在此钩子中再修改数据以防无限循环。

4. 销毁阶段
  • beforeDestroy:实例销毁前调用,可用于清理定时器、解绑全局事件等。

  • destroyed:实例已被销毁,所有指令和事件绑定已解除,需释放资源。


v-ifv-show 的区别

本质差异
  • v-if:条件为假时,真实地从 DOM 中移除元素;再次切换为真时重新渲染元素,代价是销毁与重建。

  • v-show:始终渲染元素到 DOM,仅通过切换元素的 CSS display 属性实现显示与隐藏。

适用场景
  • 频繁切换:推荐使用 v-show,因为切换只涉及样式变化,性能开销较小。

  • 很少切换:推荐使用 v-if,避免初始渲染时加载过多不必要的节点。


Redis 的持久化方式

Redis 提供了如下两种核心持久化机制,可以单独或混合使用来平衡性能与可靠性:

1. RDB(Point‐in‐Time Snapshot)
  • 在指定时间间隔触发数据快照,将整个内存数据集保存到磁盘文件(.rdb)。

  • 优点:对性能影响小,适合不要求极端数据安全的场景。

  • 缺点:快照间隔内的写入若未保存则会丢失。

2. AOF(Append Only File)
  • 将每次写命令追加记录到日志文件(.aof),服务启动时可通过重放日志恢复数据。

  • 优点:支持更多持久化策略(每秒刷盘、每操作刷盘等),可最大限度减少数据丢失。

  • 缺点:AOF 文件体积较大,重写过程对 I/O 影响较明显。

3. 混合持久化(Hybrid)
  • 在新版 Redis 中,可开启混合持久化模式,将快照与 AOF 结合,通过先写 RDB 形式的基础数据再追加 AOF 日志,提高重启效率和安全性。


什么情况下要建索引

建索引的最佳时机
  1. 设计表结构时:在已知业务场景下,预先为常用查询字段(如 WHEREJOINORDER BY 条件)创建索引,以避免后期大规模数据量导致性能瓶颈。

  2. 性能调优阶段:监控查询日志,针对慢查询中频繁访问的列或组合列,添加必要的索引以提升检索速度。大量数据增长后:当表中数据量快速增长,原有索引已无法满足查询效率需求时,应评估并补充或重建索引。

索引的注意事项
  • 避免过度索引:索引会增加写入和存储开销,应聚焦于最关键的查询场景。

  • 选择合适类型:单列索引、复合索引、全文索引等需根据查询类型和数据特点选用。

  • 定期维护:重建或优化碎片化严重的索引,以保证持续的查询性能。


InnoDB 存储引擎

  • 默认页大小 16 KB,对应单表(file-per-table)最大表空间 64 TB;可通过调整 innodb_page_size(32 KB/64 KB)扩展到 128 TB / 256 TB。

  • 单行大小受页大小与 65 535 字节行长限制约束,但不影响行数上限。

  • 行数 ≈ 表空间大小 ÷ 平均行大小(例如 1 KB/行时,64 TB ≈ 64×10¹² 行)。

MyISAM 存储引擎

  • 默认情况下,数据文件和索引文件各可增长到 256 TB,可通过 myisam_data_pointer_sizeMAX_ROWS/AVG_ROW_LENGTH 扩展至 65 536 TB(256⁷−1 字节)。

  • 行数并非硬限制

    • MySQL(无论 InnoDB 还是 MyISAM)都不对“行数”设固定上限,真正的极限由可用磁盘空间和文件系统决定。

海量数据场景下的性能保障

  • 索引:对 WHEREJOINORDER BY 等高频字段建立单列或复合索引,可在数百万行级别保持毫秒级查询。

  • 分区/分表:当表行数达到 千万级以上,或需要对历史数据做归档清理时,可通过 RANGE/HASH 分区或水平分表,将数据拆分到多个逻辑单元,提升查询和维护效率。

1. 针对 flag 字段是否建单列索引

  • 表规模:50 万 行

  • flag 分布

    • flag=1 返回 18 万(36%)

    • flag=2 返回 12 万(24%)

    • flag=3 返回 20 万(40%)

  • 索引选择性 = 不同值数 ÷ 总行数 ≈ 3/500 000 ≈ 0.0006% → 极低

  • 返回比例 均远超常见的 “10–20%” 索引使用阈值

  • 结论

    • 单列索引在这种低基数、高返回比例场景下,MySQL 优化器通常不会走索引,仍然全表扫描

    • 同时会增加写入和维护开销

    • 不建议 单独为 flag 建索引


2. 索引匹配顺序与回表机制

2.1 单列索引 vs 复合索引(最左前缀原则)
  • 单列索引:只在一列上建树,查询该列才能用上

  • 复合索引:在多列(固定顺序)上建树

    • 只有查询条件中包含最左侧那一列,才能利用该索引

    • 例如索引 (a,b,c),只支持按 aa,ba,b,c 过滤

2.2 InnoDB 的回表(Lookup)机制
  • 聚簇索引(Clustered Index):主键树上存完整行

  • 二级索引(Secondary Index):只存索引列 + 主键值

  • 回表

    • 当二级索引命中后,若查询还需其他列,就必须“回表”——再根据主键到簇索引读整行

    • 会多一次 I/O 开销

  • 覆盖索引(Covering Index):

    • 若一个索引(单列或复合)已包含了查询 SELECT/WHERE/ORDER BY 等所有列

    • 则可直接从该索引返回结果,无需回表


你知道什么情况下不走索引吗?

回答
  1. 返回行数过多(选择性低)

    • 当索引过滤后仍需读取的数据行超过表总行数的 ~20%,优化器估算“索引查找 + 回表”成本高于全表扫描,就会直接走全表扫描。

  2. 索引基数(Cardinality)或选择性(Selectivity)过低

    • 对于取值极少(如只有 2–3 种状态)的字段,索引几乎退化为全表扫描,优化器不会使用。

  3. 查询条件中无可用索引列

    • 未对 WHERE/JOIN/ORDER BY 中的列建索引,或索引列被函数包装,都会导致索引失效。

  4. 复合索引最左前缀不匹配

    • (a,b,c) 建的复合索引,若只按 b(b,c) 查询,不满足最左前缀原则,索引不生效。

  5. 小表更优

    • 当表极小(如几十行),全表扫描比索引查找更快,优化器也会选择全表扫描。

  6. 优化器剪枝(Prune)机制

    • 默认 optimizer_prune_level=1,优化器基于行数估算“剪掉”认为次优的执行计划,包括索引路径。

面试官提到 MySQL 在底层有一个以 “P” 开头的预估函数,它是怎样的?

回答
  • 这里指的正是优化器的**“Prune”**(剪枝)机制,由系统变量 optimizer_prune_level 控制:

    • MySQL 优化器会先对各种访问路径(全表扫描、索引扫描)估算访问行数,然后跳过(prune)估算成本过高的路径,以加速查询计划生成。

    • 默认 optimizer_prune_level=1(启用剪枝);若觉得优化器漏掉了更优方案,可设置 optimizer_prune_level=0 关闭剪枝。

如果在上述情况下优化器不走索引,如何强制让它走?

回答
  • 使用索引提示(Index Hint)

    SELECT * 
    FROM your_table 
    FORCE INDEX (idx_flag) 
    WHERE flag = 1;

    复制编辑

    SELECT * FROM your_table FORCE INDEX (idx_flag) WHERE flag = 1;

    FORCE INDEX 会让优化器把全表扫描成本视为“无限高”,只要指定索引可用,就会使用它。


B+Tree 的物理存储结构

  • 页(Page)单位:索引在磁盘上按 16 KB 为单位存储,每个页要么是内部节点(internal node),要么是叶子节点(leaf node)

  • 内部节点:只存放分隔键和子页指针,用于快速定位到目标叶子页;最顶层有一个固定位置的根页,其地址保存在 InnoDB 数据字典中

  • 叶子节点

    • 对于主键索引(聚簇索引),叶子页同时存放整行数据;

    • 对于二级索引(非主键索引),叶子页存放索引列值和对应的主键值,然后再回表取数据。

  • 链表结构:同一级别的页通过前后指针串成双向链表,便于范围扫描

首次查询 name='张三' 的访问流程

  1. 检查缓冲池(Buffer Pool)

    • 查询发出时,会先在内存中的缓冲池查找包含对应索引页或数据页的缓存。

  2. 未命中时从磁盘加载

    • 如果对应页不在缓冲池,就触发一次或多次磁盘 I/O,将这些页读入缓冲池。

  3. 内存中完成 B+Tree 遍历

    • 加载到内存后,再做 B+Tree 根→中间→叶子节点的搜索,定位到 张三 的记录指针(主键)或完整行。

何时直接从内存读取?

  • 缓冲池命中:若目标数据对应的页已在缓冲池中(之前被访问过或预热加载),查询会 直接在内存完成,无需任何磁盘 I/O。

  • 大多数重复或热点访问:在高并发或周期性读取同一数据时,缓冲池命中率高,性能极佳。

“po”——InnoDB 的 Buffer Pool

  • Buffer Pool(缓冲池):InnoDB 在内存中为表和索引页分配的一块大区域,用于缓存磁盘页,提升读取速度。

  • 配置建议:在专用数据库服务器上,通常将 50–80% 内存分配给缓冲池,以获得更高的命中率。


问题:MySQL 有哪几种存储引擎?

MySQL 提供了多种可选的存储引擎,以满足不同的场景需求:

  • InnoDB:默认引擎,支持事务(ACID)、行级锁、外键和崩溃恢复;适合 OLTP 高并发场景

  • MyISAM:传统引擎,查询性能较好但不支持事务与外键;适合只读或分析场景

  • MEMORY:所有数据存放在内存中,访问快速但重启后数据丢失;常用于临时表或缓存CSV:以 CSV 文本文件形式存储,便于与文本工具互通,但无索引支持

  • Archive:压缩存储大量历史数据,写入速度快但查询较慢;适合日志归档

  • MERGE:将多个 MyISAM 表合并为一个逻辑表,适用于分区模拟场景

  • FEDERATED:访问远程 MySQL 服务器上的表,提供分布式查询能力

  • NDB(Cluster):支持分布式集群,适合高可用高吞吐 OLTP 场景

问题:索引的“级别”有哪些?

MySQL 常见的索引类型(级别)包括:

  1. 主键索引(PRIMARY)

    • 每张表只能有一个,不允许 NULL,用于唯一标识行;在 InnoDB 中即聚簇索引,叶子节点存放整行数据

  2. 唯一索引(UNIQUE)

    • 保证索引列值唯一,可有多个,允许 NULL;提供唯一性约束

  3. 普通索引(INDEX)

    • 最基础的非唯一索引,可加速等值或范围查询;可在多列上创建复合索引

  4. 全文索引(FULLTEXT)

    • 基于倒排索引,用于大文本字段的自然语言检索;InnoDB 和 MyISAM 均支持,内置分词器处理关键词匹配

  5. 空间索引(SPATIAL)

    • 用于地理空间数据类型的 R-Tree 索引,仅支持 MyISAM 和部分 InnoDB 版本;加速 GIS 查询

问题:在什么场景下,应该选择 InnoDB?

  • 需要事务支持与崩溃恢复:InnoDB 支持 ACID 事务(COMMIT/ROLLBACK)和崩溃后自动恢复机制,能保证数据的一致性和可靠性,适用于对数据完整性要求高的场景。

  • 高并发写入:采用行级锁(row-level locking),减少锁冲突,适合大量并发 INSERT/UPDATE/DELETE 操作的 OLTP 系统。

  • 外键约束:原生支持外键,可在数据库层面维护表间引用完整性,无需额外应用逻辑。

问题:在什么场景下,应该选择 MyISAM?

  • 读密集型或只读分析:MyISAM 对读操作做了深度优化,若写入比例低于约 15%,使用 MyISAM 能获得更佳的查询性能。

  • 无需事务与外键:不支持事务和外键约束,结构简单,适合内容管理系统、日志检索等对完整性要求不高的场景。

  • 历史归档或快速统计:MyISAM 的表级锁在少量写入场景下影响可忽略,且其对于 COUNT(*) 等统计操作的速度优于 InnoDB,适合报表或归档库。


问题 1:MySQL(InnoDB)中的数据在磁盘上是如何存储的?
回答:

  • InnoDB 将表和索引存放在一个或多个表空间文件(.ibd)中,按B+Tree结构组织。

  • 页(Page):默认每页 16 KB(由 innodb_page_size 决定),分为内部节点(存放分隔键与子页指针)和叶子节点

  • 聚簇索引(主键索引)的叶子节点里直接存储整行数据;二级索引的叶子节点只存索引列值+主键值,再通过主键“回表”获取其余列。

  • 同一层的页通过前后指针串成双向链表,以加速范围扫描。

问题 2:什么时候会将数据加载到内存**(Buffer Pool)?
回答:

  • InnoDB 在内存中维护一个Buffer Pool,缓存表空间中的页。

  • 首次访问到某页时,如果该页不在 Buffer Pool,就触发磁盘 I/O,将页读入内存。

  • 若启用了读前置(read-ahead),InnoDB 会预读相邻页到 Buffer Pool,以提升顺序扫描效率。

  • 新加载的页通常插入到 LRU 算法的中段,以区分“初次加载”与真正的“热点”页。

问题 3:什么时候会将内存中的数据写回到磁盘?
回答:

  • 脏页刷新:Buffer Pool 中被修改但未落盘的页(脏页)由后台 Page Cleaner 线程按 LRU 策略刷写到磁盘,当脏页比例超过 innodb_max_dirty_pages_pct 时强制刷新。

  • 模糊检查点(Fuzzy Checkpoint):InnoDB 定期记录检查点,将一批脏页分批落盘,避免长时间阻塞并控制重启恢复量。

  • 事务提交日志:重做日志(Redo Log)会在提交时写入磁盘(可配置 innodb_flush_log_at_trx_commit 决定同步策略),但对应的脏页可延后刷新。

问题 4:数据或空间何时被回收?
回答:

  1. 页面淘汰(Page Eviction)

    • Buffer Pool 使用分段 LRU:将页分为“新链”和“老链”,只有反复访问后才晋升到“新链”,淘汰时优先移除“老链”尾部的页,保护真正的热点。

  2. Undo 日志清理(Purge)

    • InnoDB 的 MVCC 版本信息存放在 Undo 表空间,Purge 线程会清除不再被任何事务引用的历史版本,回收 Undo 空间。

问题 5:什么是“热点数据”,何时会被保留在内存?
回答:

  • 热点页是指在缓冲池中被频繁访问的页。

  • 在分段 LRU 中,只有在一段时间内(默认 1 秒)被至少两次访问的页,才从“老链”晋升到“新链”,被视为热点,后续不会轻易被淘汰。

  • 充足的 innodb_buffer_pool_size(通常占总内存的 50–80%)可确保大部分热点页长驻内存,提升命中率与性能。


1. Buffer Pool 是什么?

  • 缓存结构:Buffer Pool 按 页(Page) 为单位管理,每页默认大小为 16 KB(可通过 innodb_page_size 调整)

  • 内存驻留:当 InnoDB 启动时,会在内存中分配 innodb_buffer_pool_size 大小的区域,用于缓存磁盘页

  • 分段管理:内部使用分段 LRU(Least Recently Used)算法,将页分为“新链”(new sublist)和“老链”(old sublist),初次加载的页先放入老链,再根据访问频率晋升为真正的热点


2. 读操作:何时从内存 vs 磁盘

  • 缓冲池命中:执行查询时,InnoDB 首先在 Buffer Pool 查找所需页,如果存在则直接从内存读取,产生一次 Buffer Pool Read Request

  • 未命中时加载:若页不在缓冲池,则触发磁盘 I/O,将页读入内存后再完成查询,此过程计为一次 Buffer Pool Read

  • 读前置(Read-Ahead):InnoDB 可在后台预读相邻页到缓冲池,以优化顺序扫描场景,但仍需等待这些页加载完成才可命中


3. 写操作:脏页刷新与日志

  • 脏页生成:对数据页的修改首先在 Buffer Pool 中进行,并将该页标记为“脏页”(Dirty Page),同时生成重做日志(Redo Log)条目

  • 后台刷写:当脏页比例超过 innodb_max_dirty_pages_pct_lwm(默认为 10%)时,Page Cleaner 线程会自动将脏页异步写回磁盘,以维护脏页水平和释放缓冲池空间

  • 检查点机制:InnoDB 周期性地执行模糊检查点,将一定范围的脏页批量落盘,并推进日志检查点(Checkpoint),以加快崩溃恢复速度


4. 缓存管理与淘汰

  • 分段 LRU

    • 新链 vs 老链:新加载的页先放在老链头部,只有在一段时间内被多次访问,才晋升到新链

    • 淘汰策略:当缓冲池空间不足时,优先淘汰老链尾部的页,保护新链中的热点页免被挤出

  • Undo 日志清理:为支持多版本并发控制(MVCC),InnoDB 在 Undo 表空间中保存历史版本,Purge 线程会回收不再需要的 Undo 记录,避免占用过多内存和磁盘空间


5. 关键配置参数

  • innodb_buffer_pool_size:决定 Buffer Pool 总大小,建议占主机内存的 60–90% innodb_buffer_pool_instances:将 Buffer Pool 分割为多个实例,可减少高并发时的互斥锁争用,建议最多不要超过 16 个实例

  • innodb_page_cleaners:控制 Page Cleaner 线程数量,默认与 innodb_buffer_pool_instances 相同,可根据 I/O 压力调整

  • innodb_max_dirty_pages_pct_lwm:脏页低水位阈值,控制早期刷新行为,避免脏页堆积影响性能

2025.06.14

问1:Spring Boot 的核心特性是什么?它的自动装配原理又是怎样的?

答:

  1. 自动配置(Auto-Configuration)

    • Spring Boot 会根据类路径下的依赖、已定义的 Bean 以及 application.properties/YAML 等配置,自动配置常用组件,极大减少手动配置量

    • 启用入口由组合注解 @SpringBootApplication(包含 @EnableAutoConfiguration)触发,扫描并加载候选的自动配置类列表

  2. 起步依赖(Starter Dependencies)

    • 提供如 spring-boot-starter-webspring-boot-starter-data-jpa 等一键聚合常用库的 POM,简化依赖管理嵌入式服务器(Embedded Servers)

    • 内置 Tomcat、Jetty、Undertow,打包即运行,无需额外部署外部容器

  3. 外部化配置(Externalized Configuration)

    • 支持多种配置源:application.properties/YAML、环境变量、命令行参数等,实现一套代码多环境部署

  4. 生产就绪功能(Actuator)

    • 集成 Actuator 提供健康检查、指标监控、审计、日志级别动态调整等端点,便于运维管理

  5. 开发者工具(DevTools)

    • 支持热重启、LiveReload、自动重启等特性,显著提升开发效率。

  6. 约定优于配置(Convention over Configuration)

    • 默认合理,提供“开箱即用”体验,让开发者专注业务实现。

自动装配原理详解

  1. 候选自动配置类收集

    • Spring Factories 机制:启动时,SpringFactoriesLoader 扫描所有 JAR 包下的 META-INF/spring.factories(Spring Boot 2.7+ 亦支持 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)文件,加载 EnableAutoConfiguration 接口对应的实现类列表

  2. 去重与导入

    • AutoConfigurationImportSelector#getCandidateConfigurations() 方法收集并去重后,通过 ImportSelector 将合格的自动配置类导入应用上下文,生成 Bean 定义

  3. 条件注解过滤

    • 候选自动配置类上大量使用 @Conditional… 注解:

      • @ConditionalOnClass / @ConditionalOnMissingClass:基于类路径存在性决定。

      • @ConditionalOnBean / @ConditionalOnMissingBean:基于容器中 Bean 的有无决定。

      • @ConditionalOnProperty:基于配置属性值决定。

      • 还包括 @ConditionalOnSingleCandidate@ConditionalOnResource@ConditionalOnJava 等多种维度的过滤

  4. 排除与扩展

    • 排除自动配置:可在启动类使用 exclude 属性,或通过 spring.autoconfigure.exclude 配置清单屏蔽不需要的自动配置

    • 自定义自动配置:编写自定义 spring-boot-autoconfigure 模块,定义自己的 @Configuration 并在 spring.factories 中声明,即可插入自动装配流程。

  5. 调试与诊断

    • 启动时加 --debug 打印条件评估报告(Condition Evaluation Report),或通过 Actuator /conditions 端点查看哪些自动配置被加载/跳过,帮助快速定位问题。

问2:在 Spring Boot 中,如果启动时 不想 让它自动装配 Bean,该怎么办?

答:

  1. 在启动类上全局排除自动配置

    @SpringBootApplication(
      exclude = {
        DataSourceAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class
      }
    )
    public class Application { … }

    该方式会在启动时直接忽略这两个自动配置类所提供的所有 Bean

  2. 在配置文件中排除

    spring.autoconfigure.exclude=\
      org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
      org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

    与注解效果等同,适合无需改动代码即可灵活开启/关闭排除列表

  3. 测试环境中排除

    @SpringBootTest(
      exclude = { DataSourceAutoConfiguration.class }
    )
    public class RepositoryTests { … }

    在单元测试或集成测试场景,通过 exclude 屏蔽不必要的自动配置,缩短上下文启动时间

  4. 方法级可选依赖

    1. 设置 @Autowired(required = false),当对应 Bean 不存在时,Spring 不会报错:

      @Autowired(required = false)
      public void setFooService(FooService fooService) { … }

    2. 或使用 ObjectProvider<T>/Optional<T>,在缺少 Bean 时提供默认:

      @Bean
      public BarService barService(ObjectProvider<BarService> provider) {
        return provider.getIfAvailable(DefaultBarService::new);
      }

      这样可以精细控制方法注入,在容器内无相关 Bean 时自动降级处理。

  5. 自定义自动配置扩展
    若需完全替换或扩展 Spring Boot 默认行为,可在自定义的 spring-boot-autoconfigure 模块中,于 META-INF/spring.factories 声明新的配置类,并结合条件注解实现按需加载。

问3:在 Spring Boot 中,如果启动时 没有 对应的 Bean,但又 加载这个 Bean,应该怎么办?

答:

  1. 使用 @ConditionalOnMissingBean 注解

    @Configuration
    public class MyServiceConfig {
      @Bean
      @ConditionalOnMissingBean(MyService.class)
      public MyService defaultMyService() {
        return new DefaultMyService();
      }
    }

    仅在容器中 不存在 MyService 类型的 Bean 时,才会执行并注册该 defaultMyService(),确保依赖该接口的组件始终有可用实现。

  2. 注解属性详解

    • value / type:指定要检查的 Bean 类型(默认取方法返回值类型)。

    • name:按 Bean 名称匹配。

    • annotation:仅当容器中不存在带指定注解的 Bean 时生效。

    • ignored / ignoredType:排除某些候选 Bean,不参与匹配。
      默认搜索策略为 SearchStrategy.ALL,会在整个 BeanFactory 层级中查找。

  3. 与其他条件注解结合
    你可以将其与 @ConditionalOnProperty@ConditionalOnClass@ConditionalOnBean 等联合使用,实现更精细的加载控制。例如:

    @Bean
    @ConditionalOnMissingBean(MyRepo.class)
    @ConditionalOnProperty(name="app.repo.enabled", havingValue="true")
    public MyRepo defaultRepo() { … }

    仅在容器缺失 MyRepo 且配置属性 app.repo.enabled=true 时加载。

  4. 调整加载顺序
    在自动配置类上使用 @AutoConfigureAfter(OtherConfig.class)@AutoConfigureBefore,确保条件判断在或后于相关配置注册之后执行,避免由于加载时序导致的边界问题。

  5. 实践建议

    • 仅在自动配置模块中使用:将此注解限定在自动配置类,保持插件化、非侵入式特性。

    • 覆盖与优先级:结合 @Primary 或开启 spring.main.allow-bean-definition-overriding=true 控制 Bean 覆盖策略,确保自定义实现能够取代默认实现。

问4:CompletableFuture 的主要特性及常用方法

答:

核心特性

  1. 异步非阻塞(Asynchronous, Non-Blocking)

    • 通过 supplyAsync()runAsync() 将任务提交到 ForkJoinPool.commonPool() 或自定义 Executor,调用线程无需等待即可继续执行,从而实现非阻塞式并发。

  2. 链式组合(Composability)

    • 实现 CompletionStage 接口,支持 thenApply()(同步映射)、thenCompose()(扁平化异步)、thenAccept()thenRun() 等方法,可按需串联或分支异步任务流。

  3. 并行任务协调(Parallel Coordination)

    • 静态方法 allOf() 可等待多个 CompletableFuture 全部完成,anyOf() 可在任一完成时继续执行,便于多任务聚合或竞速场景。

  4. 异常处理(Error Handling)

    • 提供 exceptionally()(异常恢复)、handle()(统一处理结果与异常)、whenComplete()(副作用处理)等回调,能够优雅地捕获、补偿或记录异步执行过程中的错误。

  5. 手动完成与超时(Manual Completion & Timeouts)

    • 方法 complete()completeExceptionally() 可手动触发正常或异常完成;Java 9+ 的 orTimeout()completeOnTimeout() 支持超时后自动抛出或返回备用值。

  6. 线程安全(Thread Safety)

    • 多线程对同一 CompletableFuture 调用 complete()cancel() 等,只有首次生效,内部通过原子操作保证状态一致性。


常用方法及作用

1. 提交异步任务
  • CompletableFuture.runAsync(Runnable)
    提交无返回值任务,返回 CompletableFuture<Void>

  • CompletableFuture.supplyAsync(Supplier<U>)
    提交有返回值任务,返回 CompletableFuture<U> ,适用于需要获取结果的异步操作。

2. 同步映射与扁平化
  • thenApply(Function<T,R>)
    在前一阶段完成后,同步处理结果并返回新值,创建新的 CompletableFuture<R>

  • thenCompose(Function<T,CompletableFuture<U>>)
    将前一阶段结果传给返回 CompletableFuture<U> 的函数,避免嵌套 CompletableFuture<CompletableFuture<U>>

3. 结果消费与后续执行
  • thenAccept(Consumer<T>)
    在前一阶段完成后消费结果,不返回新值,常用于日志记录或触发事件。

  • thenRun(Runnable)
    在前一阶段完成后执行指定 Runnable,不依赖结果,适合触发独立后续逻辑。

4. 多任务合并
  • thenCombine(CompletableFuture<U>, BiFunction<T,U,R>)
    等待两个 CompletableFuture 同时完成,将它们的结果传入 BiFunction 并返回合并后的新值。

  • CompletableFuture.allOf(CompletableFuture<?>... )
    等待所有给定的 futures 完成,返回 CompletableFuture<Void>,常与 join()thenApply() 组合获取各个结果。

  • CompletableFuture.anyOf(CompletableFuture<?>... )
    返回最先完成的 future 的结果,适合竞速场景。

5. 异常与完成控制
  • exceptionally(Function<Throwable,T>)
    捕获前一阶段异常并返回备用结果,恢复链式流程。

  • handle(BiFunction<T,Throwable,R>)
    同时接收正常结果或异常,统一生成新值。

  • whenComplete(BiConsumer<T,Throwable>)
    在执行完成(正常或异常)后回调,用于日志或清理,但不改变结果。

  • complete(T) / completeExceptionally(Throwable)
    手动触发 future 成功或异常完成,用于外部控制。

  • orTimeout(long,TimeUnit) / completeOnTimeout(T,long,TimeUnit)
    在指定时间后超时抛出异常或返回默认值,简化超时处理。