好的,已按要求删除 第二部分: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 外面!
}
原因:
- 缩短临界区 :减少
putLock的持有时间,提升吞吐量。 - 避免锁传递开销 :
signalNotEmpty()需要获取takeLock,如果在putLock内获取,会造成锁的交叉等待和上下文切换。 - 解耦存与取 :
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 层的二进制日志,记录所有对数据库产生变更的操作。
核心用途:
- 主从复制:主库写入 Binlog,从库读取并重放。
- 数据恢复:配合全量备份,实现时间点恢复(Point-in-Time Recovery)。
- 数据审计:分析变更操作,排查安全问题。
三种格式:
| 格式 | 记录方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 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 线程的工作机制"等),随时告诉我。😊