MySQL
基础概念
(1) MySQL 存储引擎有哪些?常用的 InnoDB、MyISAM 区别?
MySQL 常见存储引擎有:InnoDB 、MyISAM 、Memory 、Archive 等,最常用的是 InnoDB 和 MyISAM。
| 对比维度 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | 支持事务(ACID) | 不支持事务 |
| 锁粒度 | 行级锁(基于索引) | 表级锁 |
| 外键 | 支持外键约束 | 不支持外键 |
| 崩溃恢复 | 支持(redo log) | 不支持,易损坏 |
| 全文索引 | MySQL 5.6+ 支持 | 支持 |
| COUNT(*) | 需要全表扫描 | 内部计数器,直接返回 |
| 索引结构 | B+ 树(聚簇索引) | B+ 树(非聚簇索引) |
| MVCC | 支持 | 不支持 |
| 适用场景 | 高并发、需要事务 | 读多写少、不需要事务 |
MySQL 5.5 起默认引擎从 MyISAM 改为 InnoDB,现在绝大多数场景都用 InnoDB。
(2) InnoDB 相比 MyISAM 优势在哪?
-
支持事务:InnoDB 支持 ACID 事务,可以保证数据一致性;MyISAM 不支持,一旦崩溃容易出现数据损坏。
-
行级锁:InnoDB 支持行级锁,并发性能远优于 MyISAM 的表级锁(写操作锁整张表)。
-
外键约束:InnoDB 支持外键,可以在数据库层面保证数据完整性。
-
崩溃恢复:InnoDB 通过 redo log 实现崩溃恢复,断电后能自动恢复数据;MyISAM 不行。
-
MVCC:InnoDB 支持多版本并发控制,读写不冲突,并发性能更强。
-
缓冲池:InnoDB 有 Buffer Pool 缓存数据和索引,减少磁盘 IO。
(3) 什么是事务?事务四大特性 ACID 分别是什么?
事务是一组逻辑操作的最小工作单元,要么全部执行成功,要么全部回滚,保证数据的一致性。
ACID 四大特性:
-
A(Atomicity)原子性 :事务中的操作要么全部完成,要么全部不完成。如果某个操作失败,整个事务回滚。由 undo log 实现。
-
C(Consistency)一致性:事务执行前后,数据库从一个一致状态转变到另一个一致状态。一致性是最终目的,由其他三个特性共同保证。
-
I(Isolation)隔离性 :多个并发事务之间互不干扰,一个事务的中间状态对其他事务不可见。由 锁 + MVCC 实现。
-
D(Durability)持久性 :事务一旦提交,对数据的修改就是永久性的,即使系统崩溃也不会丢失。由 redo log 实现。
(4) 数据库三范式是什么?日常开发一定要严格遵守吗?
-
第一范式(1NF):每个字段都是不可再分的最小原子单元。例如"地址"字段不应包含省、市、区,应拆分。
-
第二范式(2NF):在满足 1NF 的基础上,非主键字段必须完全依赖于主键,不能只依赖主键的一部分。主要针对联合主键的情况。
-
第三范式(3NF):在满足 2NF 的基础上,非主键字段之间不能有传递依赖关系。即非主键字段只能依赖于主键,不能依赖于其他非主键字段。
日常开发不一定要严格遵守 。为了查询性能,经常会进行反范式化设计:
-
适当增加冗余字段,减少多表 JOIN 操作
-
例如订单表中冗余存储用户名、商品名,虽然违反 3NF,但避免了频繁 JOIN
-
核心原则:在数据一致性和查询性能之间取得平衡
(5) 执行一条 SQL 语句,期间都发生了哪些事情?
以查询语句为例,完整流程:
-
连接器:客户端发起连接请求,验证用户名、密码、权限。
-
查询缓存(MySQL 8.0 已移除):检查查询缓存,若命中直接返回结果。
-
解析器:词法分析 → 语法分析,生成抽象语法树(AST),检查 SQL 语法是否正确。
-
预处理器:检查表名、字段名是否存在,处理通配符(SELECT *)。
-
优化器:选择最优的执行计划,包括选择使用哪个索引、JOIN 顺序等。
-
执行器:调用存储引擎接口,按照执行计划执行查询。
-
存储引擎:InnoDB 执行实际的数据读取(先从 Buffer Pool 找,找不到从磁盘读),返回数据给执行器。
-
返回结果:执行器将结果集返回给客户端。
对于更新语句,还涉及:WAL 机制(先写 redo log prepare → 写 binlog → 提交 redo log)。
事务(超级高频)
(6) MySQL 四大事务隔离级别分别是什么?
| 隔离级别 | 英文 | 说明 |
|---|---|---|
| 读未提交 | READ UNCOMMITTED | 能读到其他事务未提交的数据,最低级别 |
| 读已提交 | READ COMMITTED | 只能读到其他事务已提交的数据(Oracle 默认) |
| 可重复读 | REPEATABLE READ | 同一事务内多次读取结果一致(MySQL/InnoDB 默认) |
| 串行化 | SERIALIZABLE | 所有事务串行执行,最高级别,性能最差 |
(7) 脏读、不可重复读、幻读分别是什么?
-
脏读(Dirty Read) :事务 A 读到了事务 B 还未提交的数据。如果 B 回滚,A 读到的就是脏数据。
-
不可重复读(Non-Repeatable Read) :事务 A 两次读同一行数据,中间事务 B 修改并提交了 该行,导致 A 两次读到不同的值 。侧重于修改。
-
幻读(Phantom Read) :事务 A 两次用相同条件查询,中间事务 B 插入了新行 ,导致 A 第二次查到了多出来的"幻影行" 。侧重于新增/删除。
(8) 四大隔离级别分别能解决哪些问题?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | ✗ 不能解决 | ✗ 不能解决 | ✗ 不能解决 |
| 读已提交 | ✓ 解决 | ✗ 不能解决 | ✗ 不能解决 |
| 可重复读 | ✓ 解决 | ✓ 解决 | ✗ 不能完全解决 |
| 串行化 | ✓ 解决 | ✓ 解决 | ✓ 解决 |
(9) InnoDB 默认隔离级别是什么?
InnoDB 默认隔离级别是 可重复读(REPEATABLE READ)。
可以通过命令查看和修改:
SELECT @@transaction_isolation; -- 查看当前隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 修改
MySQL 选择 RR 而不是 RC 作为默认级别,主要是因为在 RR 级别下配合 MVCC + Next-Key Lock 可以很大程度上解决幻读问题。
(10) 什么是幻读?怎么解决幻读?
幻读:事务 A 在同一个事务内,用相同条件查询两次,第二次查到了第一次没有的"幻影行",这是因为事务 B 在中间插入了新数据并提交。
解决方案:
-
快照读(普通 SELECT) :通过 MVCC 解决。事务 A 读取的是事务开始时的快照版本,其他事务插入的数据对 A 不可见。
-
当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE) :通过 Next-Key Lock(临键锁) 解决。临键锁 = 记录锁 + 间隙锁,锁住索引记录及其前后的间隙,阻止其他事务插入新行。
-
串行化隔离级别:直接串行执行所有事务,彻底避免幻读,但性能极差。
(11) 事务的实现原理:MVCC 是什么?
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现高并发读写的核心机制。
核心思想 :每行数据保存多个历史版本,读操作可以读取旧版本,不需要加锁,从而实现读写不冲突。
MVCC 依赖的三大组件:
-
隐式字段:DB_TRX_ID(最近修改的事务ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏主键)。
-
undo log 版本链:每次修改数据时,将旧版本写入 undo log,通过回滚指针串联成版本链。
-
Read View(读视图):事务执行快照读时生成,用于判断版本链中哪个版本对当前事务可见。
MVCC 只在 读已提交(RC) 和 可重复读(RR) 两个隔离级别下工作。
(12) 可重复读隔离级别,完全解决幻读了吗?
没有完全解决。
在 RR 隔离级别下:
-
快照读(普通 SELECT):MVCC 可以避免幻读,因为读的是快照。
-
当前读 + Next-Key Lock:大部分场景可以避免幻读。
但存在一个经典问题:如果事务 A 先执行快照读,然后事务 B 插入并提交,事务 A 再执行当前读(如 UPDATE),此时 MVCC 快照会"看到"B 插入的行,产生幻读。
所以 RR 级别下只能最大程度减少幻读,不能完全消除。要彻底解决幻读需要串行化隔离级别。
MVCC
(13) MVCC 底层原理是什么?
MVCC 底层由三部分组成:
1. 数据行的隐式字段
-
DB_TRX_ID(6字节):最后一次修改该行的事务 ID。
-
DB_ROLL_PTR(7字节):回滚指针,指向 undo log 中该行的上一个版本。
-
DB_ROW_ID(6字节):如果表没有主键,InnoDB 自动生成的隐藏主键。
2. undo log 版本链
-
每次 UPDATE/DELETE 操作会将旧版本数据写入 undo log。
-
通过 DB_ROLL_PTR 将多个历史版本串成链表。
-
链表头部是最新版本,尾部是最旧版本。
3. Read View(读视图)
-
快照读时生成 Read View,包含四个关键字段:
-
m_ids:生成 Read View 时所有活跃(未提交)事务 ID 列表
-
min_trx_id:m_ids 中最小值
-
max_trx_id:下一个将分配的事务 ID(即最大事务 ID + 1)
-
creator_trx_id:创建该 Read View 的事务 ID
可见性判断规则:
-
DB_TRX_ID < min_trx_id:版本在 Read View 创建前已提交 → 可见
-
DB_TRX_ID >= max_trx_id:版本在 Read View 创建后才出现 → 不可见
-
DB_TRX_ID 在 [min_trx_id, max_trx_id) 之间:
-
如果在 m_ids 中 → 不可见(事务还未提交)
-
如果不在 m_ids 中 → 可见(已提交)
-
如果当前版本不可见,沿版本链找更旧的版本,直到找到可见版本或到链尾。
(14) 什么是快照读、当前读?举例说明
快照读(Snapshot Read):
-
读取 MVCC 版本链中的历史版本数据,不加锁。
-
普通的 SELECT 语句就是快照读。
SELECT * FROM user WHERE id = 1; -- 快照读
当前读(Current Read):
-
读取数据库中最新的已提交数据,并加锁。
-
以下语句是当前读:
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 加排他锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- 加共享锁
INSERT INTO user VALUES (1, 'Tom'); -- 插入操作
UPDATE user SET name = 'Tom' WHERE id = 1; -- 更新操作
DELETE FROM user WHERE id = 1; -- 删除操作
快照读保证可重复读,当前读保证读到最新数据但会产生锁竞争。
(15) 隐式字段 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 作用
| 字段 | 大小 | 作用 |
|---|---|---|
| DB_TRX_ID | 6 字节 | 记录最后一次修改(INSERT/UPDATE)该行的事务 ID。MVCC 判断版本可见性时用到。 |
| DB_ROLL_PTR | 7 字节 | 回滚指针,指向 undo log 中该行的上一个历史版本,用于构建版本链。 |
| DB_ROW_ID | 6 字节 | 隐藏自增行 ID。当表没有定义主键时,InnoDB 自动生成作为聚簇索引的主键。如果有主键则不需要此字段。 |
(16) undo log 日志作用、版本链是什么?
undo log 的作用:
-
事务回滚:存储数据修改前的旧值,回滚时用旧值恢复数据(实现原子性)。
-
MVCC 多版本控制:保存数据的历史版本,供快照读使用。
版本链:
-
每次修改数据时,InnoDB 将旧版本写入 undo log 段。
-
新版本的 DB_ROLL_PTR 指向旧版本,形成一条从新到旧的链表。
-
版本链头部是最新版本 (当前数据行),尾部是最旧版本。
-
快照读时,从版本链头部开始,根据 Read View 判断哪个版本可见。
(17) Read View 视图四个字段作用、可见性规则
四个字段:
| 字段 | 含义 |
|---|---|
| m_ids | 创建 Read View 时,所有未提交的事务 ID 列表 |
| min_trx_id | m_ids 中的最小值 |
| max_trx_id | 系统即将分配的下一个事务 ID(当前最大事务 ID + 1) |
| creator_trx_id | 创建该 Read View 的当前事务 ID |
可见性规则(对版本链中某版本的 DB_TRX_ID 判断):
-
DB_TRX_ID == creator_trx_id :自己修改的版本 → ✅ 可见
-
DB_TRX_ID < min_trx_id :在 Read View 创建前已提交 → ✅ 可见
-
DB_TRX_ID >= max_trx_id :在 Read View 创建后才出现 → ❌ 不可见
-
min_trx_id <= DB_TRX_ID < max_trx_id:
-
若 DB_TRX_ID 在 m_ids 中 → ❌ 不可见(创建时该事务还未提交)
-
若 DB_TRX_ID 不在 m_ids 中 → ✅ 可见(创建时该事务已提交)
如果当前版本不可见,沿着版本链继续找更早的版本,直到找到可见版本。
(18) MVCC 怎么实现可重复读、读已提交?
两者的核心区别在于 Read View 的生成时机不同:
读已提交(RC):
-
每次 执行快照读(SELECT)都会重新生成 Read View。
-
所以每次读都能读到最新已提交的数据。
-
其他事务在两次读之间提交了修改,就能读到不同的值 → 产生不可重复读。
可重复读(RR):
-
只在事务中第一次 执行快照读时生成 Read View,之后复用同一个 Read View。
-
所以整个事务期间读到的数据都是一样的 → 实现可重复读。
-
其他事务提交的修改对当前事务始终不可见。
简单记忆:RC 每次读都新建 Read View,RR 只在第一次读时新建。
索引
(19) InnoDB 索引结构为什么选 B+ 树,不选二叉树、红黑树、B 树?
不选二叉树/红黑树:
-
树的高度太高,每个节点只存一个数据,查询需要大量磁盘 IO。
-
例如 1000 万条数据,二叉树高度约 23 层,每次查询可能要 23 次磁盘 IO。
不选 B 树:
-
B 树每个节点都存数据(key + value),导致非叶子节点能存的索引数量更少,树更高。
-
B 树的范围查询需要中序遍历,效率低。
选 B+ 树的原因:
-
非叶子节点只存索引,不存数据:一个节点能存更多索引,树更矮。通常 3-4 层的 B+ 树可以存储千万级数据,查询只需 3-4 次磁盘 IO。
-
数据全部存在叶子节点:查询性能稳定,每次都走到叶子节点。
-
叶子节点之间用双向链表连接:天然支持范围查询,只需遍历链表即可。
-
磁盘预读友好:操作系统按页读取,B+ 树节点大小设置为一个页(默认 16KB),一次 IO 读一个节点。
(20) B+ 树和 B 树的区别?
| 对比 | B 树 | B+ 树 |
|---|---|---|
| 数据存储 | 所有节点都存数据 | 只有叶子节点存数据,非叶子节点只存索引 |
| 叶子节点连接 | 叶子节点之间没有指针 | 叶子节点通过双向链表连接 |
| 范围查询 | 需要中序遍历整棵树,效率低 | 只需在叶子节点链表上顺序遍历 |
| 查询稳定性 | 可能在中间节点就找到,不稳定 | 每次都要走到叶子节点,性能稳定 |
| 非叶子节点容量 | 因为存数据,每个节点索引少 | 只存索引,每个节点索引多,树更矮 |
(21) 聚簇索引和非聚簇索引(二级索引)区别?
| 对比 | 聚簇索引(主键索引) | 非聚簇索引(二级索引) |
|---|---|---|
| 叶子节点存什么 | 存整行数据 | 存索引列的值 + 主键值 |
| 数量 | 一张表只有一个 | 可以有多个 |
| 数据顺序 | 数据按聚簇索引顺序物理存储 | 不影响数据物理存储顺序 |
| 查询 | 直接拿到整行数据 | 拿到主键后需要回表到聚簇索引查完整行 |
InnoDB 每张表都有且仅有一个聚簇索引(如果有主键就用主键,没有就用隐藏 ROW_ID)。
(22) 主键索引、唯一索引、普通索引、联合索引区别
| 索引类型 | 是否允许重复 | 是否允许 NULL | 说明 |
|---|---|---|---|
| 主键索引 | 不允许重复 | 不允许 NULL | 一张表只能有一个,InnoDB 聚簇索引的基础 |
| 唯一索引 | 不允许重复 | 允许 NULL(多个NULL) | 保证列值唯一性 |
| 普通索引 | 允许重复 | 允许 NULL | 最基本的索引,加快查询速度 |
| 联合索引 | 取决于类型 | 取决于类型 | 多个列组合的索引,遵循最左前缀原则 |
(23) 什么是回表查询?怎么避免回表?
回表查询 :通过二级索引查到了主键值,但需要的列数据不在二级索引的叶子节点中,需要拿着主键到聚簇索引中再查一次,这个过程叫回表。
例如:表有 id(主键)、name、age,在 name 上建了二级索引:
SELECT * FROM user WHERE name = 'Tom';
-- 先在 name 索引找到 id → 再回主键索引查整行数据 → 回表
避免回表的方法:
- 覆盖索引:查询的列都包含在索引中,不需要回表。
> -- 建立联合索引 (name, age)
SELECT name, age FROM user WHERE name = 'Tom'; -- 不需要回表
- 减少 SELECT 列:只查需要的列,避免 SELECT *。
(24) 覆盖索引是什么?使用场景?
覆盖索引:查询所需的所有列都能从索引中直接获取,不需要回表到聚簇索引。
判断标志 :EXPLAIN 中 Extra 列显示 Using index。
使用场景:
-
只需要查少数几个字段时,为这些字段建立联合索引。
-
统计查询:SELECT COUNT(*) FROM user(利用主键索引扫描比全表扫描快)。
-
分页查询优化:
> -- 利用覆盖索引先拿到 id,再 JOIN 原表
SELECT * FROM user t1
INNER JOIN (SELECT id FROM user ORDER BY name LIMIT 1000000, 10) t2
(25) 最左前缀原则原理、为什么要遵守?
最左前缀原则 :联合索引 (a, b, c) 在 B+ 树中的排列顺序是先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。
因此查询时必须从最左列开始匹配:
-
WHERE a = 1 → ✅ 走索引
-
WHERE a = 1 AND b = 2 → ✅ 走索引
-
WHERE a = 1 AND b = 2 AND c = 3 → ✅ 走索引
-
WHERE b = 2 → ❌ 不走索引(缺少最左列 a)
-
WHERE b = 2 AND c = 3 → ❌ 不走索引
-
WHERE a = 1 AND c = 3 → ⚠️ 只能用到 a 的索引(MySQL 5.0+ 优化器可能会用索引下推)
为什么:因为 B+ 树是按联合索引的列顺序排列的,跳过左边的列,右边的列就不是有序的了,无法利用 B+ 树的有序性进行二分查找。
MySQL 8.0 新特性 :支持索引跳跃扫描(Index Skip Scan),在某些条件下即使不满足最左前缀也能使用索引,但效率不如完整匹配。
(26) 索引下推原理和作用
索引下推(Index Condition Pushdown, ICP) 是 MySQL 5.6 引入的优化。
没有 ICP 时:
-
联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 10
-
存储引擎根据 name LIKE '张%' 找到所有匹配的主键 → 全部回表 → Server 层再过滤 age = 10
有 ICP 时:
-
存储引擎在遍历索引时,直接判断索引中 age 字段是否等于 10
-
不满足条件的记录直接跳过,减少回表次数
作用:减少回表查询的次数,提升查询性能。
EXPLAIN 中 Extra 显示 Using index condition 表示使用了索引下推。
(27) 什么是索引失效?哪些情况会导致索引失效?
索引失效:建立了索引但查询时没有使用索引,而是走全表扫描。
常见索引失效场景:
-
违反最左前缀原则:联合索引跳过左边的列。
-
对索引列使用函数或运算:WHERE YEAR(create_time) = 2024,WHERE id + 1 = 5。
-
隐式类型转换:字段是 varchar 类型,查询条件传了数字 WHERE phone = 13800138000。
-
LIKE** 以通配符开头**:WHERE name LIKE '%张'(全表扫描)。
-
OR** 条件中有非索引列**:WHERE indexed_col = 1 OR non_indexed_col = 2。
-
NOT IN **、**NOT EXISTS:在某些场景下优化器放弃索引。
-
IS NULL ** / **IS NOT NULL:某些情况下优化器认为全表扫描更快。
-
范围查询右边的列:WHERE a > 1 AND b = 2,联合索引 (a,b) 中 b 无法使用索引。
-
!=** 或 ****<>**:优化器可能认为全表扫描更高效。
(28) 模糊查询 like %xxx、like xxx% 哪个走索引?
-
LIKE 'xxx%'(右边模糊)→ ✅ 走索引。因为 B+ 树是按前缀排序的,xxx 开头的数据是连续的一段。
-
LIKE '%xxx'(左边模糊)→ ❌ 不走索引。因为无法确定前缀,需要全表扫描。
-
LIKE '%xxx%'(两边模糊)→ ❌ 不走索引。
如果一定要用左模糊查询的优化方案:
-
使用全文索引(FULLTEXT INDEX)。
-
使用 Elasticsearch 等搜索引擎。
-
反转字符串存储 + 右模糊查询(适用于特定场景)。
(29) 字段类型隐式转换为什么会导致索引失效?
当查询条件的值类型与字段类型不一致时,MySQL 会进行隐式类型转换。
例如:phone 字段是 VARCHAR 类型,查询 WHERE phone = 13800138000(数字类型)。
MySQL 会将字符串转为数字进行比较,等价于:
WHERE CAST(phone AS SIGNED) = 13800138000
这就相当于对索引列使用了函数,破坏了 B+ 树的有序性,导致索引失效,只能全表扫描。
解决方案:
-
保证查询条件类型与字段类型一致。
-
WHERE phone = '13800138000'(加引号)。
字符串转数字 会导致索引失效;但数字转字符串(字段是数字类型,查询传字符串)通常不影响索引。
(30) 为什么不建议用 SELECT *?
-
无法利用覆盖索引:SELECT * 查出所有列,二级索引不包含所有列,必须回表,增加 IO 开销。
-
网络传输浪费:查出很多不需要的字段,占用更多网络带宽。
-
内存消耗增大:MySQL 需要将更多数据放入 Buffer Pool / 结果集缓存中。
-
代码可读性差:不明确需要哪些字段,不利于代码维护。
-
字段变更风险:表结构变动(新增大字段)可能导致 SELECT * 查询变慢甚至出错。
(31) 联合索引创建顺序原则:区分度高、长度小、经常查询
创建联合索引时列的顺序建议遵循:
- 区分度高的列放左边:区分度 = 不重复值的比例。区分度越高,过滤效果越好。
-
例如身份证号区分度 > 性别区分度。
-
可以用 SELECT COUNT(DISTINCT col) / COUNT(*) FROM table 计算。
-
字段长度小的放左边:索引页能存更多索引项,减少树的高度,降低 IO 次数。
-
经常用于查询条件的列优先:最常用的查询条件放最左,满足最左前缀原则。
示例:(user_id, create_time) 比 (create_time, user_id) 更优,因为 user_id 区分度高且更常用于查询。
(32) 什么时候不适合建索引?
-
数据量很小的表:几百条数据,全表扫描比走索引还快。
-
频繁增删改的列:索引维护成本高,每次 DML 都要更新索引。
-
区分度低的列:如性别(男/女)、状态(0/1),过滤效果差,建索引意义不大。
-
很少出现在 WHERE 中的列:不用于查询条件的列建索引是浪费。
-
TEXT、BLOB 等大字段:索引体积太大,效率低(可以用前缀索引)。
-
表更新频率远大于查询频率:索引带来的查询收益不如维护成本。
锁
(33) InnoDB 有哪些锁?行锁、表锁、意向锁
按粒度分类:
| 锁类型 | 说明 |
|---|---|
| 表级锁 | 锁住整张表。包括表共享读锁(S)、表排他写锁(X)、意向锁 |
| 行级锁 | 锁住具体行。包括记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock) |
| 页级锁 | 介于表锁和行锁之间,锁住一页数据(BDB 引擎,InnoDB 不用) |
按功能分类:
-
共享锁(S 锁 / 读锁):允许事务读取一行,多个事务可同时持有。
-
排他锁(X 锁 / 写锁):允许事务更新或删除一行,独占访问。
意向锁(Intention Lock):
-
意向共享锁(IS):事务打算给行加共享锁前,先给表加 IS 锁。
-
意向排他锁(IX):事务打算给行加排他锁前,先给表加 IX 锁。
-
作用:快速判断表中是否有行锁,避免逐行检查,提升加表锁的效率。
(34) 行锁什么时候变表锁?
InnoDB 的行锁是加在索引上的,不是加在行数据上的。以下情况行锁会退化为表锁:
- 没有走索引 :如果 UPDATE/DELETE 的 WHERE 条件没有使用索引,InnoDB 会扫描全表,对每一行加锁,等效于表锁。
> UPDATE user SET name = 'Tom' WHERE age = 18; -- age 没有索引 → 表锁
- 使用了全表扫描:即使有索引,但优化器判断全表扫描更高效时,也会退化为表锁。
解决方案:确保 WHERE 条件中的列有索引,避免行锁变表锁。
(35) 记录锁、间隙锁、临键锁分别是什么?
这三种锁都是 InnoDB 在可重复读(RR)隔离级别下用于解决幻读问题的行锁类型:
| 锁类型 | 英文名 | 锁住范围 | 说明 |
|---|---|---|---|
| 记录锁 | Record Lock | 只锁索引记录本身 | 精确锁住某一行,如 id = 5 |
| 间隙锁 | Gap Lock | 锁住索引记录之间的间隙 | 不包含记录本身,如 (5, 10) |
| 临键锁 | Next-Key Lock | 记录锁 + 间隙锁 | 左开右闭区间,如 (5, 10]。InnoDB RR 下默认使用 |
示例:索引中有 5、10 两个值:
-
记录锁:锁住 5 或 10
-
间隙锁:锁住 (5, 10) 之间的间隙
-
临键锁:锁住 (0, 5]、(5, 10] 这样的区间
在读已提交(RC) 隔离级别下,InnoDB 只有记录锁,没有间隙锁和临键锁。
(36) 临键锁怎么解决幻读?
场景:事务 A 执行 SELECT * FROM t WHERE id > 5 FOR UPDATE(当前读)。
临键锁的工作过程:
-
对 id = 10 加记录锁 + (5, 10] 的临键锁。
-
对 id > 10 到正无穷的间隙也加间隙锁 (10, +∞)。
-
事务 B 如果想 INSERT INTO t VALUES (8),会落在 (5, 10] 的临键锁范围内 → 被阻塞。
-
事务 B 如果想 INSERT INTO t VALUES (15),落在 (10, +∞) 间隙锁范围 → 被阻塞。
这样,在当前读的场景下,临键锁锁住了查询范围 + 间隙,阻止其他事务在范围内插入新数据,从而解决了幻读问题。
(37) 什么是死锁?产生条件、怎么排查和避免死锁?
死锁:两个或多个事务互相持有对方需要的锁,形成循环等待,谁都无法继续执行。
产生条件(四个必要条件):
-
互斥条件:资源不能共享,一个事务占用的资源其他事务不能访问。
-
持有并等待:事务持有某些资源的同时还在等待其他资源。
-
不可剥夺:事务已获得的资源不能被强制剥夺。
-
循环等待:多个事务形成首尾相接的等待环。
排查方法:
- 查看最近一次死锁信息:
> SHOW ENGINE INNODB STATUS;
- 开启死锁日志,记录每次死锁:
> SET GLOBAL innodb_print_all_deadlocks = ON;
- 查看当前锁等待情况:
> SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
避免方法:
-
按固定顺序访问表和行:避免循环等待。
-
缩短事务:事务越短,持锁时间越少。
-
降低隔离级别:使用 RC 级别,没有间隙锁,减少死锁概率。
-
合理使用索引:确保走索引加行锁,避免退化为表锁。
-
设置锁等待超时:innodb_lock_wait_timeout(默认 50 秒)。
-
InnoDB 自带死锁检测:检测到死锁时自动回滚持有最少排他锁的事务。
日志
(38) MySQL 三大日志:redo log、undo log、binlog 各自作用
| 日志 | 所属层 | 作用 |
|---|---|---|
| redo log | InnoDB 引擎层 | 崩溃恢复。记录数据页的物理修改("在某个数据页做了什么"),保证事务持久性(D)。 |
| undo log | InnoDB 引擎层 | 事务回滚 + MVCC。记录数据的逻辑反向操作("修改前的旧值"),保证事务原子性(A)。 |
| binlog | MySQL Server 层 | 主从复制 + 数据归档。记录所有修改数据的逻辑语句,用于从库同步和数据恢复。 |
redo log 是 InnoDB 特有的;binlog 是所有存储引擎共有的(Server 层)。
(39) redo log 为什么能保证事务崩溃恢复?WAL 机制
WAL(Write-Ahead Logging,预写式日志):事务提交时,先把修改写入 redo log 文件,再慢慢刷到磁盘数据页。
redo log 保证崩溃恢复的原理:
-
事务执行时,数据修改先写入 Buffer Pool(内存)。
-
同时将修改操作写入 redo log buffer(内存)。
-
事务提交时,redo log 刷入磁盘(fsync),此时即使数据页还没刷盘也没关系。
-
如果 MySQL 崩溃,重启时会读取 redo log,**重做(redo)**已提交但未刷盘的数据页修改。
redo log 的结构:
-
固定大小的循环文件(如 2 个文件,每个 1GB)。
-
有 write pos(写入位置)和 checkpoint(已刷盘位置)两个指针。
-
write pos 追上 checkpoint 时,说明日志满了,需要阻塞,强制刷盘推进 checkpoint。
核心思想:用顺序写日志代替随机写数据页,IO 效率更高。
(40) binlog 是什么?格式 statement/row/mixed 区别
binlog(归档日志) 是 MySQL Server 层的逻辑日志,记录所有修改数据的 SQL 语句,用于主从复制 和数据恢复。
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 原始 SQL 语句 | 日志量小 | NOW()、UUID() 等函数导致主从不一致 |
| ROW | 每行数据修改的前后值 | 不会主从不一致 | 批量操作时日志量巨大 |
| MIXED | 混合模式:普通 SQL 用 STATEMENT,不安全的用 ROW | 兼顾两者 | 仍可能有少量不一致场景 |
生产环境一般用 ROW 格式,保证数据一致性,配合 binlog 压缩策略控制日志大小。
(41) redo log 和 binlog 区别?
| 对比 | redo log | binlog |
|---|---|---|
| 所属层 | InnoDB 引擎层 | MySQL Server 层 |
| 日志内容 | 物理日志("某个数据页的某个偏移做了什么修改") | 逻辑日志(SQL 语句或行数据变更) |
| 文件大小 | 固定大小,循环写入 | 追加写入,一个文件写满后新建 |
| 用途 | 崩溃恢复 | 主从复制、数据归档恢复 |
| 写入时机 | 事务执行过程中持续写入 | 事务提交时写入 |
| 格式 | 循环文件 | 追加文件 |
(42) 事务提交时 redo log、binlog 两阶段提交原理
为什么需要两阶段提交:redo log 和 binlog 是两个独立的日志,如果不用两阶段提交,崩溃恢复时可能出现两个日志不一致的情况。
两阶段提交流程:
- prepare 阶段:
- InnoDB 将 redo log 写入磁盘,标记为 prepare 状态
- 写 binlog:
- Server 层将 binlog 写入磁盘
- commit 阶段:
- InnoDB 将 redo log 标记为 commit 状态
崩溃恢复时的判断逻辑:
-
redo log = commit → 直接提交事务 ✅
-
redo log = prepare + binlog 完整 → 补全 commit,提交事务 ✅
-
redo log = prepare + binlog 不完整 → 回滚事务 ❌
-
redo log 不完整 → 回滚事务 ❌
两阶段提交保证了 redo log 和 binlog 的一致性,是 MySQL 崩溃恢复 + 主从复制双重保障的关键。
SQL 优化
(43) 一条 SQL 执行完整流程?
查询语句(SELECT):
客户端 → 连接器 → 查询缓存(8.0前) → 解析器 → 预处理器 → 优化器 → 执行器 → 存储引擎 → 返回结果
更新语句(UPDATE):
客户端 → 连接器 → 解析器 → 优化器 → 执行器 → 存储引擎
存储引擎:将数据页读入 Buffer Pool → 修改数据 → 写 redo log (prepare)
Server 层:写 binlog
存储引擎:提交 redo log (commit) → 后台异步刷脏页到磁盘
→ 返回客户端
更新语句不是直接写磁盘,而是先改内存(Buffer Pool),通过 WAL 机制保证持久性。
(44) explain 执行计划每个字段含义
EXPLAIN SELECT * FROM user WHERE name = 'Tom' 的结果:
| 字段 | 含义 |
|---|---|
| id | 查询序号。id 相同从上往下执行;id 不同,id 越大优先级越高 |
| select_type | 查询类型:SIMPLE(简单查询)、PRIMARY(最外层)、SUBQUERY(子查询)、DERIVED(派生表)、UNION |
| table | 访问的表名 |
| type(重要) | 访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL |
| possible_keys | 可能使用的索引 |
| key(重要) | 实际使用的索引 |
| key_len | 索引使用的字节数,可判断联合索引用到了哪些列 |
| rows | 预估扫描行数,越小越好 |
| Extra(重要) | Using index(覆盖索引)、Using where(Server层过滤)、Using filesort(需优化)、Using temporary(需优化)、Using index condition(索引下推) |
(45) type 执行效率级别
从优到劣排列:
| type | 说明 | 示例 |
|---|---|---|
| system | 表只有一行记录(系统表) | SELECT * FROM mysql.user WHERE ... |
| const | 通过主键或唯一索引等值查询,最多返回一行 | WHERE id = 1 |
| eq_ref | 多表 JOIN 时,被驱动表通过主键/唯一索引等值匹配 | JOIN ON t1.id = t2.id(t2.id 是主键) |
| ref | 通过普通索引等值查询,可能返回多行 | WHERE name = 'Tom'(name 有索引) |
| range | 索引范围扫描 | WHERE id > 5 AND id < 10 |
| index | 全索引扫描(扫描整棵索引树) | SELECT id FROM user(覆盖索引) |
| ALL | 全表扫描 | SELECT * FROM user WHERE ...(无索引可用) |
优化目标:至少达到 range 级别,最好能达到 ref。
(46) 怎么看懂慢查询日志?
开启慢查询日志:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录下来
慢查询日志关键字段:
Time: 2024-01-01T10:00:00.000000Z
User@Host: rootroot @ localhost 127.0.0.1
Query_time: 3.200000 Lock_time: 0.000100 Rows_sent: 1 Rows_examined: 1000000
SELECT * FROM user WHERE age = 18;
-
Query_time:查询耗时(超过 long_query_time 才记录)。
-
Lock_time:等待锁的时间。
-
Rows_sent:返回给客户端的行数。
-
Rows_examined:扫描的行数(越大说明效率越低)。
分析工具:使用 mysqldumpslow 或 pt-query-digest 进行聚合分析,找出 TOP N 慢 SQL。
(47) 慢查询优化整体思路?
-
开启慢查询日志:先定位哪些 SQL 慢。
-
使用 EXPLAIN 分析执行计划:查看 type、key、rows、Extra,确认是否走了预期的索引。
-
优化索引:
-
缺少索引 → 添加合适的索引
-
索引失效 → 改写 SQL 让索引生效
-
回表太多 → 利用覆盖索引
- 优化 SQL 语句:
-
避免 SELECT *,只查需要的列
-
避免子查询,改写为 JOIN
-
优化深分页(limit 大偏移量)
- 优化表结构:
-
适当反范式化,减少 JOIN
-
大表考虑分库分表
- 优化配置参数:Buffer Pool 大小、排序缓冲区等。
(48) limit 分页深偏移量怎么优化?(limit 1000000, 10)
问题:LIMIT 1000000, 10 实际上扫描了 1000010 行,然后丢弃前 100 万行,只返回 10 行。效率极低。
优化方案:
方案一:延迟关联(推荐)
-- 利用覆盖索引先快速定位 id,再 JOIN 原表
SELECT * FROM user t1
INNER JOIN (
SELECT id FROM user ORDER BY id LIMIT 1000000, 10
子查询只扫描主键索引(覆盖索引),速度远快于扫描整行数据。
方案二:记录上次位置
-- 记住上次最后一条的 id,下次从该 id 开始查
SELECT * FROM user WHERE id > 上次最大id ORDER BY id LIMIT 10;
适用于顺序翻页场景,每次都是 O(10) 的扫描量。
方案三:业务限制
-
限制最大翻页深度,如只允许翻前 100 页。
-
搜索引擎(如百度)也是只显示前几十页。
(49) order by 排序原理、什么时候会 Using filesort?怎么优化?
MySQL 两种排序方式:
-
全字段排序(全字段在 sort_buffer 中排序):把需要的字段都放入 sort_buffer,在内存中排序后直接返回。
-
rowid 排序:只把排序字段 + 主键 id 放入 sort_buffer,排序后根据 id 回表取完整数据。
Using filesort 出现条件:
-
排序字段没有索引,或排序字段与查询条件不是同一个索引。
-
使用了 ORDER BY 但无法利用索引的有序性。
-- name 没有索引 → Using filesort
SELECT * FROM user ORDER BY name;
优化方法:
- 给排序字段加索引:
> CREATE INDEX idx_name ON user(name);
-
利用联合索引:如果 WHERE 和 ORDER BY 的字段可以组成联合索引,排序可以利用索引有序性。
-
增大 sort_buffer_size:减少磁盘临时文件的使用。
-
减少排序数据量:加 WHERE 条件缩小范围。
(50) group by 原理与优化思路
GROUP BY 原理:
-
MySQL 对 GROUP BY 字段进行排序(或利用索引有序性),然后对相同值的行进行聚合计算。
-
如果没有索引,MySQL 会使用临时表 + filesort 来完成分组,性能较差。
优化思路:
-
给 GROUP BY 字段加索引:利用索引的有序性避免额外排序。
-
使用联合索引:(a, b) 索引可以支持 WHERE a = 1 GROUP BY b。
-
关闭隐式排序:GROUP BY col ORDER BY NULL,如果不需要排序,避免额外排序开销。
-
增大 tmp_table_size 和 max_heap_table_size:让临时表尽量在内存中完成。
-
减少 GROUP BY 的列数:分组的列越少,性能越好。
(51) join 连接原理:内连接、左连接、右连接
内连接(INNER JOIN):返回两个表中匹配的行。
SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.t1_id;
-- 只返回 t1 和 t2 都匹配的行
左连接(LEFT JOIN):返回左表所有行 + 右表匹配的行(不匹配补 NULL)。
SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id;
-- t1 全部返回,t2 不匹配的填 NULL
右连接(RIGHT JOIN):返回右表所有行 + 左表匹配的行(不匹配补 NULL)。
JOIN 执行算法:
-
Index Nested-Loop Join(NLJ) :被驱动表的关联字段有索引。驱动表逐行扫描,利用索引在被驱动表中查找。最高效。
-
Block Nested-Loop Join(BNL):被驱动表无索引。把驱动表的数据放入 join_buffer,然后全表扫描被驱动表进行匹配。
-
Hash Join(MySQL 8.0.18+):用驱动表构建 hash 表,扫描被驱动表时通过 hash 查找。
优化原则:小表做驱动表,大表做被驱动表;被驱动表关联字段要有索引。
(52) 大表 join 怎么优化?
-
确保被驱动表的 JOIN 字段有索引:让 JOIN 走 Index Nested-Loop Join,避免全表扫描。
-
小表驱动大表:MySQL 优化器通常会自动选择小表作为驱动表。可用 STRAIGHT_JOIN 强制指定。
-
增大 join_buffer_size:当无法走索引时(BNL),更大的 buffer 可以减少对被驱动表的扫描次数。
-
减少驱动表数据量:在 JOIN 前先 WHERE 过滤,减少参与 JOIN 的行数。
-
只 SELECT 需要的列:减少内存和网络开销。
-
分库分表:数据量实在太大时,考虑水平拆分。
-
考虑用宽表 / 冗余字段:减少 JOIN 需求,用空间换时间。