面试题高优版

好的,已按要求删除 第二部分:JVM 内存调优(Q8、Q9)。

更新后的面试题合集如下:


第一部分:并发编程(线程池 + 阻塞队列)

Q1:ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?各自适用什么场景?

参考答案:

维度 ArrayBlockingQueue LinkedBlockingQueue
底层结构 数组(连续内存) 链表(节点分散)
锁机制 一把锁 (ReentrantLock) 两把锁 (takeLock + putLock)
存/取能否并行 ❌ 不能(同一把锁) ✅ 能(不同锁)
容量 必须指定,有界 可指定,默认 Integer.MAX_VALUE(无界)
性能(高并发) 较低 更高
适用场景 低并发、需要精确控制内存 高并发、多生产者多消费者

追问:为什么 LinkedBlockingQueue 性能更高?

因为存和取操作使用不同的锁,可以真正并行。同时使用 AtomicInteger 无锁地维护 count,避免了两把锁之间需要额外同步。


Q2:ConcurrentHashMap 如何实现线程安全的计数器?

参考答案:

使用 ConcurrentHashMap 做计数器时,不能写成 map.put(key, map.get(key) + 1),因为 读-改-写 不是原子操作。

推荐写法:

java 复制代码
// 方式一:使用 compute(原子操作)
counter.compute(key, (k, v) -> v == null ? 1 : v + 1);

// 方式二:使用 AtomicInteger 作为值(性能更高)
AtomicInteger ai = counter.computeIfAbsent(key, k -> new AtomicInteger(0));
ai.incrementAndGet();

追问:为什么 compute 是线程安全的?

ConcurrentHashMap.compute() 在操作期间,会锁住该 key 对应的桶(bucket),保证 读取旧值 → 执行计算 → 写入新值 整个过程是原子性的。


Q3:线程池的队列有哪些?如何影响线程池行为?

参考答案:

队列 特点 对线程池的影响
LinkedBlockingQueue 有界/无界,吞吐量高 无界时 maxPoolSize 失效
ArrayBlockingQueue 有界,性能稳定 队列满后才创建非核心线程
SynchronousQueue 容量为0,直接交接 立即创建新线程,直到 maxPoolSize
PriorityBlockingQueue 按优先级出队 无界,任务按优先级执行
DelayQueue 延迟出队 用于定时/延迟任务

追问:生产环境为什么推荐使用有界队列?

无界队列可能导致任务无限堆积,最终 OOM。有界队列配合合理的拒绝策略,可以保护系统稳定性。


Q4:阻塞队列中 notEmpty 和 notFull 是如何工作的?

参考答案:

它们是 ArrayBlockingQueue 内部的两个 Condition 条件变量,配合 ReentrantLock 使用:

条件 关联角色 await() 时机 signal() 时机
notEmpty 消费者 队列为空时,消费者等待 生产者放入元素后,唤醒消费者
notFull 生产者 队列满时,生产者等待 消费者取出元素后,唤醒生产者

追问:为什么用 while 而不是 if 来检查条件?

防止虚假唤醒(spurious wakeup) ,线程从 await() 返回后必须重新检查条件是否满足。


Q5:LinkedBlockingQueue 中为什么在锁外执行 signalNotEmpty()?

参考答案:

put() 方法在入队后,如果发现旧 count == 0(队列之前为空),需要唤醒消费者。这个唤醒操作被设计在 putLock 外部执行:

java 复制代码
if (c == 0) {
    signalNotEmpty();  // 在 putLock 外面!
}

原因:

  1. 缩短临界区 :减少 putLock 的持有时间,提升吞吐量。
  2. 避免锁传递开销 :signalNotEmpty() 需要获取 takeLock,如果在 putLock 内获取,会造成锁的交叉等待和上下文切换。
  3. 解耦存与取 :putLock 只管入队,takeLock 只管唤醒,职责清晰。

第二部分:数据库(InnoDB)

Q6:Redo Log 和 Binlog 有什么区别?

维度 Redo Log Binlog
所属层 InnoDB 存储引擎 MySQL Server 层
日志内容 物理日志(页级修改) 逻辑日志(SQL 语句/行变更)
写入方式 循环写入,覆盖 追加写入,保留历史
用途 崩溃恢复(保证持久性) 主从复制、数据恢复

Q7:为什么需要 Redo Log?它解决了什么问题?

参考答案:

Redo Log 解决的核心矛盾是:Buffer Pool 的高性能 vs 数据的持久性。

  • 如果每次修改都直接写数据页 :16KB 数据页是随机 I/O,极慢(每次需要寻道)。
  • 如果只写 Buffer Pool:内存易失,宕机会丢失数据。

解决方案 :事务提交时,将修改操作以 顺序追加 的方式写入 Redo Log(顺序 I/O,极快),数据页异步刷盘。宕机后通过重放 Redo Log 恢复数据。

这就是 WAL(Write-Ahead Logging) 的核心思想:先写日志,再写数据。


Q8:Buffer Pool 中的脏页什么时候刷到磁盘?

参考答案:

脏页刷盘是异步 的,由后台线程触发,不随事务提交而刷盘。

触发方式 说明
定时触发 page_cleaner 线程默认每秒执行一次
定量触发 脏页比例超过 innodb_max_dirty_pages_pct(默认 90%)
Redo Log 写满 需要推进 Checkpoint,强制刷盘(会阻塞所有用户线程)
Buffer Pool 空间不足 LRU 淘汰时,脏页需先刷盘再淘汰
MySQL 正常关闭 所有脏页刷盘后才能退出
手动触发 FLUSH TABLES 等命令

关键结论 :事务提交只保证 Redo Log 落盘,不保证脏页落盘。


Q9:innodb_flush_log_at_trx_commit 三个值的区别?

值 行为 安全性 性能
1(默认) 每次提交,write() + fsync() ✅ 最高,不丢数据 ❌ 最差
2 每次提交,仅 write() 到 OS Cache,每秒 fsync() ⚠️ 宕机丢 1 秒数据 ✅ 较好
0 提交时不写,后台线程每秒 write() + fsync() ⚠️ 宕机丢 1 秒数据 ✅ 最好

核心 :write() 只是将数据从程序内存拷贝到 OS Cache,fsync() 才是真正落盘。


Q10:有了 Redo Log,MySQL 宕机还会丢数据吗?

参考答案:

  • innodb_flush_log_at_trx_commit = 1(默认):不会丢任何已提交事务的数据。
  • = 2 或 = 0:可能丢失最近 1 秒内已提交事务的数据。

所以答案是:默认情况下不会丢,但如果为了性能调整了参数,就可能丢。


Q11:Buffer Pool、Redo Log Buffer、OS Cache 有什么区别?

参考答案:

维度 Buffer Pool Redo Log Buffer OS Cache
位置 InnoDB 进程内存 InnoDB 进程内存 操作系统内核内存
存储内容 完整数据页(16KB) Redo Log 记录(512B 日志块) 文件页缓存
作用 读写缓存,避免磁盘 IO 事务持久化,顺序写入优化 所有文件 IO 的中转站
刷盘时机 异步(后台线程) 事务提交时触发 由 fsync() 或 OS 决定
数据安全性 易失(宕机丢失) 易失(需 fsync() 落盘) 易失(断电丢失)

第三部分:架构与设计

Q12:状态机(StateMachine)中"有状态"和"无状态"如何理解?

  • 有状态 :服务会 记住 之前的信息。例如 Spring StateMachine 的一个实例保存了当前状态,下次事件触发时不需要传入当前状态。
  • 无状态 :服务 记不住 ,每次请求必须带齐所有信息。例如 COLA 每次调用都需要传入 currentState,状态机本身只是一个查表的工具。

类比:

  • 有状态 = 老主顾面馆:老板记住你的口味
  • 无状态 = 标准化面馆:每次来都要重新点单

Q13:规则引擎 Easy Rules 和表达式引擎 Aviator 的区别?

维度 Easy Rules Aviator
定位 轻量级规则引擎 高性能表达式引擎
解决问题 替代 if...else,管理独立规则 动态计算公式、表达式求值
动态性 弱(修改需重启) 强(表达式可存储外部,热更新)
性能 毫秒级 极高(编译为字节码)
适用场景 促销规则、数据校验 动态公式、风控评分

好的,已按要求删除 Q7 及之后的所有题目。

更新后的面试题合集如下:


第三部分:数据库(InnoDB 进阶)

Q1:Undo Log 是什么?什么时候生成?

参考答案:

Undo Log 是 InnoDB 存储引擎为实现事务的原子性(Atomicity) 和 MVCC(多版本并发控制) 而维护的一份日志。

核心内容:

  • 记录的是逻辑日志 。INSERT 对应一条 DELETE,UPDATE 对应一条反向 UPDATE,用于回滚。
  • 存储位置:共享表空间(ibdata1)或独立的 Undo 表空间,持久化在磁盘上。

生成时机:

  • INSERT 操作 :生成 TRX_UNDO_INSERT_REC,记录主键信息,用于回滚时删除。
  • UPDATE 操作 :生成 TRX_UNDO_UPD_EXIST_REC,记录修改前的旧版本数据。
  • DELETE 操作 :生成类似 UPDATE 的 Undo Log,标记删除并记录旧版本。

关键时机点:

  • 在数据页被修改之前生成(先写日志,后写数据)。
  • 事务执行期间:每次 DML 操作都会立即生成对应的 Undo Log。
  • 事务提交时 :不会立即删除,需要保留到没有更早的事务需要看到这些旧版本 时,由后台 Purge 线程清理。

Q2:Undo Log 和 MVCC 的关系是什么?

参考答案:

Undo Log 是 MVCC 的数据基石。

  • 版本链 :每一行数据被修改时,旧版本写入 Undo Log,行头隐藏字段 DB_ROLL_PTR 指向 Undo Log,形成版本链。
  • Read View:事务查询时生成 Read View,记录活跃事务 ID 列表。
  • 可见性判断 :InnoDB 从当前版本开始,通过 DB_ROLL_PTR 沿版本链回溯,找到第一个符合可见性规则的版本。

#mermaid-svg-PZWkw7OIEHxrhmI4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PZWkw7OIEHxrhmI4 .error-icon{fill:#552222;}#mermaid-svg-PZWkw7OIEHxrhmI4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PZWkw7OIEHxrhmI4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .marker.cross{stroke:#333333;}#mermaid-svg-PZWkw7OIEHxrhmI4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PZWkw7OIEHxrhmI4 p{margin:0;}#mermaid-svg-PZWkw7OIEHxrhmI4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster-label text{fill:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster-label span{color:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster-label span p{background-color:transparent;}#mermaid-svg-PZWkw7OIEHxrhmI4 .label text,#mermaid-svg-PZWkw7OIEHxrhmI4 span{fill:#333;color:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .node rect,#mermaid-svg-PZWkw7OIEHxrhmI4 .node circle,#mermaid-svg-PZWkw7OIEHxrhmI4 .node ellipse,#mermaid-svg-PZWkw7OIEHxrhmI4 .node polygon,#mermaid-svg-PZWkw7OIEHxrhmI4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .rough-node .label text,#mermaid-svg-PZWkw7OIEHxrhmI4 .node .label text,#mermaid-svg-PZWkw7OIEHxrhmI4 .image-shape .label,#mermaid-svg-PZWkw7OIEHxrhmI4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PZWkw7OIEHxrhmI4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .rough-node .label,#mermaid-svg-PZWkw7OIEHxrhmI4 .node .label,#mermaid-svg-PZWkw7OIEHxrhmI4 .image-shape .label,#mermaid-svg-PZWkw7OIEHxrhmI4 .icon-shape .label{text-align:center;}#mermaid-svg-PZWkw7OIEHxrhmI4 .node.clickable{cursor:pointer;}#mermaid-svg-PZWkw7OIEHxrhmI4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .arrowheadPath{fill:#333333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PZWkw7OIEHxrhmI4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PZWkw7OIEHxrhmI4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PZWkw7OIEHxrhmI4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster text{fill:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 .cluster span{color:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PZWkw7OIEHxrhmI4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PZWkw7OIEHxrhmI4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PZWkw7OIEHxrhmI4 .icon-shape,#mermaid-svg-PZWkw7OIEHxrhmI4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PZWkw7OIEHxrhmI4 .icon-shape p,#mermaid-svg-PZWkw7OIEHxrhmI4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PZWkw7OIEHxrhmI4 .icon-shape .label rect,#mermaid-svg-PZWkw7OIEHxrhmI4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PZWkw7OIEHxrhmI4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PZWkw7OIEHxrhmI4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PZWkw7OIEHxrhmI4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DB_ROLL_PTR
DB_ROLL_PTR
当前数据行

DB_TRX_ID=101
Undo Log

版本1

DB_TRX_ID=100
Undo Log

版本0

DB_TRX_ID=99

一句话 :Undo Log 存数据,Read View 定规则,两者结合实现 MVCC。


Q3:什么是"快照读"和"当前读"?举例说明。

参考答案:

维度 快照读(Snapshot Read) 当前读(Current Read)
读取版本 历史版本(事务开始时或 Read View 生成时) 最新已提交版本
是否加锁 不加锁 加锁(共享锁或排他锁)
典型操作 普通 SELECT SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT

举例:

sql 复制代码
-- 事务 A
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;  -- ① 快照读: 1000

-- 事务 B 修改并提交
UPDATE account SET balance = 800 WHERE id = 1; COMMIT;

-- 事务 A 再次查询
SELECT balance FROM account WHERE id = 1;          -- ② 快照读: 1000(可重复读)
SELECT balance FROM account WHERE id = 1 FOR UPDATE; -- ③ 当前读: 800

Q4:快照读和当前读分别适用于什么场景?

参考答案:

场景 推荐读取方式 理由
报表统计、数据分析 快照读 数据量大,避免加锁阻塞
商品/文章详情页 快照读 读多写少,高并发下性能优先
库存扣减、余额扣减 当前读 (FOR UPDATE) 必须读取最新数据,防止超卖
执行 UPDATE / DELETE 当前读(自动) 必须基于最新数据版本操作
防重复提交、唯一性校验 当前读 (LOCK IN SHARE MODE) 避免并发场景下的重复写入

Q5:Binlog 是什么?有几种格式?

参考答案:

Binlog(Binary Log) 是 MySQL Server 层的二进制日志,记录所有对数据库产生变更的操作。

核心用途:

  1. 主从复制:主库写入 Binlog,从库读取并重放。
  2. 数据恢复:配合全量备份,实现时间点恢复(Point-in-Time Recovery)。
  3. 数据审计:分析变更操作,排查安全问题。

三种格式:

格式 记录方式 优点 缺点 适用场景
STATEMENT 记录 SQL 语句 日志量小,节省 I/O 非确定性函数可能导致主从不一致 数据一致性要求不高的场景
ROW 记录每行数据的变更 数据一致性高(MySQL 8.0+ 默认) 日志量可能较大 金融、交易等强一致性场景
MIXED MySQL 自动切换 兼顾性能与一致性 行为有一定复杂性 通用业务场景,稳妥选择

Q6:什么是 MySQL 的二阶段提交(2PC)?解决了什么问题?

参考答案:

MySQL 的二阶段提交是 InnoDB 为了协调 Redo Log 和 Binlog 两份日志之间的一致性,而引入的内部机制。

解决的问题:Redo Log 写了但 Binlog 没写(或反过来),导致主从不一致。

流程:

复制代码
阶段一(Prepare):
  ① 执行 SQL,在 Buffer Pool 中修改数据页
  ② 写入 Redo Log,状态标记为 prepare
  ③ 写入 Binlog

阶段二(Commit):
  ④ Binlog 写入成功后,将 Redo Log 状态更新为 commit
  ⑤ 根据参数刷盘,返回"提交成功"

崩溃恢复判断逻辑:

Redo Log 状态 Binlog 状态 恢复决策
prepare 不存在 回滚
prepare 存在且完整 提交
commit 存在且完整 提交

💡 总结

以上涵盖了你问过的所有数据库知识点,按 Undo Log → MVCC → 当前读/快照读 → Binlog → 二阶段提交 递进组织。

如果还想补充某个问题的追问细节(比如"二阶段提交如何判断 Binlog 是否完整"、"Purge 线程的工作机制"等),随时告诉我。😊

相关推荐
苍何2 小时前
开源微信流 Windows,微信聊天记录,可以直接给 Codex 和 Obsidan 了
后端
代码什么用4 小时前
Spring基础使用
java·后端·spring
明月_清风4 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
蜗牛互联网5 小时前
Java 17 HttpClient调用文件转写API的超时与失败回退
java·人工智能·后端
用户69371750013847 小时前
2026,程序员的时代拐点到了
android·前端·后端
惜鸟7 小时前
从源码看 pi 的上下文管理:原始数据永久保留,模型视角按需裁剪
后端
量化分析码农7 小时前
【Python量化策略评估体系 #05】极端行情扛得住吗?2008/2015/2020 三场景压力测试
后端
十年Java程序媛7 小时前
Java 接口和抽象类对比|Java8 新特性,抛弃老旧八股,正确选型
java·spring boot·后端
IT_陈寒7 小时前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
用户8356290780518 小时前
使用 Python 对 Excel 工作表进行排序
后端·python