数据库 - MySQL 面试八股文
本文档整理了后端开发面试中关于 MySQL 数据库最常被问到的高频面试题和深度解析,重点聚焦于 InnoDB 存储引擎。
一、 MySQL 基础架构与执行流程
1. MySQL 的逻辑架构是怎样的?
MySQL 的逻辑架构大致分为两层:
- Server 层:涵盖了 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等)。所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
- 连接器:负责跟客户端建立连接、获取权限、维持和管理连接。
- 查询缓存:MySQL 8.0 已废弃。之前用于缓存执行过的 SQL 语句及其结果。
- 分析器:对 SQL 进行词法分析和语法分析,检查 SQL 是否合法。
- 优化器:决定使用哪个索引,或者在一个语句有多表关联时决定各个表的连接顺序。
- 执行器:调用存储引擎的 API 接口来执行 SQL,并返回结果。
- 存储引擎层:负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。MySQL 5.5.5 版本开始,InnoDB 成为了默认存储引擎。
2. 一条 SQL 语句在 MySQL 中是如何执行的?
- 客户端通过连接器与 MySQL Server 建立连接。
- (MySQL 8.0 之前)查询缓存,命中则直接返回,否则继续。
- 分析器进行词法和语法分析,判断 SQL 是否正确。
- 优化器生成执行计划,选择最优的索引。
- 执行器检查用户权限,调用存储引擎接口进行数据的读写,最后将结果返回给客户端。
二、 存储引擎
3. InnoDB 和 MyISAM 的主要区别是什么?
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务 (Transaction) | 支持 | 不支持 |
| 锁级别 | 行级锁(默认)、表级锁 | 仅支持表级锁 |
| 外键 (Foreign Key) | 支持 | 不支持 |
| 崩溃恢复 | 支持(通过 Redo Log 实现安全恢复) | 不支持 |
| 索引结构 | 聚簇索引(数据和主键索引绑在一起) | 非聚簇索引(索引和数据分离) |
| 适用场景 | 绝大多数场景,特别是需要事务和高并发读写 | 以读为主,对事务完整性要求不高的老旧系统 |
三、 索引机制(绝对重点)
4. 什么是索引?MySQL 为什么默认使用 B+ 树作为索引结构?
索引是帮助 MySQL 高效获取数据的数据结构(就像书的目录)。
为什么使用 B+ 树?(对比其他结构)
- 对比 Hash 索引:Hash 虽然单条查询最快(O(1)),但不支持范围查询(>、<、between)、不支持模糊匹配和最左前缀原则。
- 对比 二叉搜索树 / 红黑树:树的层级太深,会导致磁盘 I/O 次数过多,查询效率低下。
- 对比 B 树:B 树的所有节点都存储数据,而 B+ 树只有叶子节点存储数据,非叶子节点只存键值。因此 B+ 树一个节点可以容纳更多的键,树的高度更低(通常 3-4 层),磁盘 I/O 更少。同时,B+ 树的叶子节点用双向链表连接,非常适合范围查询。
5. 什么是聚簇索引和非聚簇索引(二级索引)?什么是回表?
- 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据。在 InnoDB 中,通常主键就是聚簇索引(如果没有主键,会选择第一个非空的唯一索引;如果还没有,会隐式生成一个 row_id 作为聚簇索引)。
- 非聚簇索引(二级索引):叶子节点不存储完整的行数据,而是存储主键的值。
- 回表:如果通过二级索引查询数据,由于二级索引叶子节点只存了主键值,要想获取除主键外的其他字段数据,就需要拿着这个主键值回到聚簇索引中再查一次,这个过程叫做“回表”。
6. 什么是覆盖索引、最左前缀原则、索引下推?
- 覆盖索引:在一个查询中,如果一个索引包含了(或覆盖了)所有需要查询的字段的值,就不需要回表查询了,这叫覆盖索引。(例如 SELECT id, name FROM table WHERE name = ‘张三’,如果给 name 建了索引,因为二级索引本身包含主键 id,所以直接在二级索引就能拿到结果,无需回表)。
- 最左前缀原则:在联合索引中(如 (a, b, c)),查询条件必须从索引的最左前列开始并且不跳过中间的列。即可以匹配 (a)、(a, b)、(a, b, c),但不能直接匹配 (b) 或 (b, c)。
- 索引下推 (ICP):MySQL 5.6 引入的优化。在联合索引匹配时,如果遇到范围查询(如 >),其后的字段无法走索引。但在索引下推优化下,存储引擎会在遍历索引时,直接把判断条件“下推”给存储引擎层进行过滤,减少了回表的次数。
7. 导致索引失效的常见场景有哪些?
- 使用了 SELECT *(极易导致不走覆盖索引而全表扫描)。
- 对索引列进行了计算、函数操作或类型转换(如 WHERE DATE(create_time) = ‘2023-01-01’ 或隐式类型转换如字符串传了数字)。
- 使用了前导模糊查询(如 LIKE ‘%abc’,但 LIKE ‘abc%’ 是走索引的)。
- 违反最左前缀原则(联合索引没有使用第一列)。
- 使用了 OR,且 OR 前后不是都有索引。
- MySQL 优化器认为全表扫描比走索引成本更低时(如表数据极少,或查询的数据占全表很大比例)。
四、 事务与并发控制
8. 事务的 ACID 特性是什么?底层是如何保证的?
- 原子性 (Atomicity):事务是最小执行单位,不可分割,要么全成功,要么全失败。由 Undo Log(回滚日志)保证,失败时执行回滚。
- 隔离性 (Isolation):并发访问时,一个事务的执行不影响其他事务。由 MVCC(多版本并发控制)和 锁 保证。
- 持久性 (Durability):事务一旦提交,对数据的改变是永久的。由 Redo Log(重做日志)保证(WAL 机制,先写日志后写磁盘)。
- 一致性 (Consistency):事务执行前后,数据保持一致状态(如转账前后总金额不变)。一致性是目的,由原子性、隔离性、持久性共同保证。
9. 并发事务会带来哪些问题?(脏读、不可重复读、幻读)
- 脏读:事务 A 读到了事务 B 已修改但未提交的数据。如果 B 回滚,A 读到的就是脏数据。
- 不可重复读:事务 A 两次读取同一条数据,期间事务 B 修改并提交了该数据,导致 A 两次读到的数据不一致。(侧重于数据被修改或删除)。
- 幻读:事务 A 执行两次相同的范围查询,期间事务 B 插入了新的满足条件的数据并提交,导致 A 第二次查出了更多的“幻影”行。(侧重于数据被新增)。
10. MySQL 的事务隔离级别有哪些?默认是哪个?
- 读未提交 (Read Uncommitted):最低级别,会发生脏读、不可重复读、幻读。
- 读已提交 (Read Committed, RC):解决了脏读,但会发生不可重复读和幻读。(Oracle 默认)。
- 可重复读 (Repeatable Read, RR):解决了脏读、不可重复读。在 InnoDB 中,通过 MVCC 和 Next-Key Lock 很大程度上解决了幻读问题。(MySQL 默认隔离级别)。
- 串行化 (Serializable):最高级别,强制事务串行执行,解决所有问题,但性能极差。
五、 MVCC (多版本并发控制)
11. 什么是 MVCC?它是如何工作的?
MVCC (Multi-Version Concurrency Control) 是 InnoDB 实现隔离级别的一种核心机制,主要用于处理读写冲突,做到读写不阻塞(无锁读)。
MVCC 主要由三个组件实现:
- 隐藏字段:InnoDB 会在每行数据后面增加两个隐藏列:
- trx_id:最近一次修改该行数据的事务 ID。
- roll_pointer:回滚指针,指向 Undo Log 中该行的历史版本。
- Undo Log(版本链):记录数据修改前的状态。每次更新数据时,旧数据会被存入 Undo Log,并通过 roll_pointer 串联成一个单链表(版本链)。
- Read View(读视图):事务进行快照读(普通的 SELECT)时产生的一个数据结构,用来判断当前事务能看到版本链中的哪个版本。
12. Read View 的核心机制是什么?RC 和 RR 下的区别?
Read View 包含四个核心字段:
- creator_trx_id:创建该 Read View 的事务 ID。
- m_ids:创建 Read View 时,当前系统中**活跃的(未提交的)**事务 ID 列表。
- min_trx_id:m_ids 中的最小值。
- max_trx_id:系统将分配给下一个事务的 ID。
可见性判断规则(基于被访问行的 trx_id):
- 如果 trx_id < min_trx_id:说明修改该行的事务在生成 Read View 前已提交,可见。
- 如果 trx_id >= max_trx_id:说明修改该行的事务在生成 Read View 之后才开始,不可见。
- 如果 min_trx_id <= trx_id < max_trx_id:
- 若 trx_id 在 m_ids 列表中:说明修改该行的事务还在活跃(未提交),不可见。
- 若 trx_id 不在 m_ids 列表中:说明修改该行的事务已经提交,可见。
- 如果不可见,就顺着版本链 (roll_pointer) 往下找,直到找到可见的版本。
RC 和 RR 的核心区别:
- RC(读已提交):每次执行 SELECT 时都会生成一个新的 Read View。因此能读到其他事务新提交的数据,产生不可重复读。
- RR(可重复读):只在事务第一次执行 SELECT 时生成一个 Read View,之后的整个事务期间都复用这个 Read View。因此同一事务内多次读取结果相同,实现了可重复读。
六、 MySQL 锁机制
13. InnoDB 的行级锁有哪些?
InnoDB 的行锁是通过给索引项加锁来实现的,而不是给表的行记录加锁。
- Record Lock(记录锁):仅仅锁住某一条记录。
- Gap Lock(间隙锁):锁定一个范围,但不包括记录本身。目的是为了防止其他事务在这个范围内插入数据,从而解决幻读。
- Next-Key Lock(临键锁):Record Lock + Gap Lock 的结合,锁定一个范围,并且锁定记录本身(左开右闭区间)。InnoDB RR 隔离级别默认使用 Next-Key Lock。
14. 什么是死锁?如何解决?
- 死锁:两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
- 解决/预防:
- InnoDB 内部有死锁检测机制(innodb_deadlock_detect=on),检测到死锁会自动回滚代价较小的事务。
- 业务层面预防:以固定的顺序访问表和行数据。
- 大事务拆小,缩短事务持有锁的时间。
- 为表添加合理的索引,避免全表扫描退化为表锁,减少锁冲突的概率。
七、 核心日志 (Binlog / Redo Log / Undo Log)
15. 说说 Binlog、Redo Log、Undo Log 的区别?
- Undo Log (回滚日志):
- 作用:实现事务的原子性和 MVCC。
- 内容:逻辑日志,记录数据的历史版本(比如你执行了 INSERT,它就记录一条对应的 DELETE)。
- Redo Log (重做日志):
- 作用:实现事务的持久性(Crash-safe)。MySQL 宕机重启后,靠它恢复未落盘的数据。
- 内容:物理日志,记录了“在某个数据页上做了什么修改”。
- 特点:InnoDB 引擎特有,大小固定,循环写入(WAL 机制,Write-Ahead Logging)。
- Binlog (归档日志):
- 作用:用于数据备份和主从复制。
- 内容:逻辑日志,记录的是 SQL 语句(或行修改的数据)。
- 特点:Server 层实现,所有引擎共有,追加写入,不会覆盖旧日志。
16. 什么是两阶段提交 (Two-Phase Commit)?为什么需要它?
在执行更新语句时,Redo Log 和 Binlog 必须保持数据一致性。MySQL 采用两阶段提交来保证:
- Prepare 阶段:将更新写入 Redo Log,将 Redo Log 标记为 prepare 状态。
- Commit 阶段:将更新写入 Binlog,然后将 Redo Log 标记为 commit 状态。
为什么需要两阶段提交?
如果在写完 Redo Log 还没写 Binlog 时发生宕机(或者反过来),会导致主库(依靠 Redo Log 恢复)和从库(依靠 Binlog 同步)的数据不一致。两阶段提交配合内部的事务 XID,确保了崩溃恢复时,Binlog 和 Redo Log 的状态绝对一致。