MySQL 50+ 道高频面试题(含详细答案)

第一梯队:必考高频题(出现率 80%+)


1. InnoDB 和 MyISAM 的区别?

核心答案:

InnoDB 是 MySQL 默认存储引擎,支持事务行级锁外键MVCC崩溃恢复

MyISAM 是早期默认引擎,不支持事务 ,使用表级锁 ,崩溃后无法安全恢复,适合只读或读多写少的场景。

详细说明:

InnoDB 通过 redo log 实现崩溃恢复,即使突然断电,重启后也能恢复已提交的事务。它的行级锁 大大提高了并发写入能力,MVCC 让读操作不阻塞写操作。

MyISAM 的表级锁 意味着任何写操作都会锁住整张表,并发写入性能差。但它有全文索引(InnoDB 在 5.6 版本后也支持了),在某些读多写少的场景仍有应用。

生产建议 :除非有特殊原因,否则都使用 InnoDB


2. 索引的底层结构为什么用 B+ 树?

核心答案:

B+ 树矮胖树高通常只有 2 到 4 层磁盘 I/O 次数少 。叶子节点形成有序链表范围查询高效 。非叶子节点只存键值,单节点存储更多索引项

详细说明:

与其他数据结构对比:

二叉树 / AVL / 红黑树:数据量大时树高过高,比如 2000 万行数据,红黑树高度可能达到 20 多层,查询需要 20 多次磁盘 I/O,性能极差。

B 树:非叶子节点也存储数据,每个节点能存的索引项少,树高比 B+ 树高。而且范围查询需要回溯到父节点,效率低。

哈希表 :虽然单点查询 O(1),但不支持范围查询 ,不支持排序,无法应对 WHERE age > 18 这类查询。

B+ 树的优势 :一个节点通常 16KB,每个索引项约 16 字节,一个节点可存约 1000 个索引项。2000 万行数据,树高仅需 3 层,查询只需 3 次磁盘 I/O。叶子节点的双向链表让范围查询变成链表遍历,非常高效。


3. 聚簇索引和非聚簇索引的区别?

核心答案:

聚簇索引 的叶子节点存储完整的行数据 ,一张表只能有一个,主键索引就是聚簇索引。

非聚簇索引 (二级索引)的叶子节点存储主键值 ,查询时需要回表到聚簇索引查完整数据。

详细说明:

InnoDB 中,数据本身就是按聚簇索引组织的。如果你没有定义主键,InnoDB 会选择一个唯一非空索引作为聚簇索引;如果也没有,则隐式生成一个 6 字节的 rowid 作为聚簇索引。

回表 的过程:比如在 name 字段上建了索引,执行 SELECT * FROM t WHERE name = '张三'。先在 name 索引树上找到 '张三' 对应的主键 id,再拿着 id 到聚簇索引树上查找完整行数据。这个过程需要两次 B+ 树查找

覆盖索引 可以避免回表:如果查询的字段全部在索引中,比如 SELECT id, name FROM t WHERE name = '张三',name 索引已经包含了 id 和 name,就直接返回,不需要回表,性能大幅提升。

自增主键 的优势:使用自增主键时,新数据按顺序插入,只会在当前页末尾追加,不会触发页分裂。如果用 UUID 作为主键,插入是随机的,会频繁导致页分裂,造成性能下降和空间浪费。


4. 事务的四大特性(ACID)及实现原理?

核心答案:

  • 原子性(Atomicity) :通过 undo log 实现,事务失败时回滚到修改前状态

  • 一致性(Consistency):由 A、I、D 共同保证,加上业务逻辑约束

  • 隔离性(Isolation) :通过 锁 + MVCC 实现,防止事务间相互干扰

  • 持久性(Durability) :通过 redo log 实现,崩溃恢复时不丢失已提交事务

详细说明:

undo log 实现原子性 :当事务修改数据时,InnoDB 会把修改前的旧值记录到 undo log 中。如果事务需要回滚,就读取 undo log 把数据恢复原状。undo log 也用于 MVCC:当其他事务需要读取旧版本数据时,通过 undo log 构建历史快照。

redo log 实现持久性 :采用 WAL(Write-Ahead Logging)预写日志 机制。修改数据时,先写 redo log(顺序写) ,再写磁盘数据文件(随机写)。顺序写的速度比随机写快很多。如果数据库崩溃,重启后通过 redo log 重放已经写入 redo log 但尚未刷盘的事务,保证数据不丢失。

WAL 的核心 :redo log 是物理日志,记录的是"在某个数据页的某个偏移量做了某某修改"。它采用循环写 的方式,有两个文件轮流写,写满一个就切换。checkpoint 机制保证 redo log 不会被覆盖掉还没有刷盘的数据。


5. MySQL 的隔离级别及解决的问题?

核心答案:

MySQL 默认隔离级别是 REPEATABLE READ(可重复读) ,通过 Next-Key Lock(行锁 + 间隙锁) 解决了幻读问题。

四种隔离级别从低到高:

  • READ UNCOMMITTED(读未提交):可能产生脏读、不可重复读、幻读

  • READ COMMITTED(读已提交):解决脏读,仍可能产生不可重复读和幻读

  • REPEATABLE READ(可重复读):解决脏读和不可重复读,InnoDB 下也解决了幻读

  • SERIALIZABLE(串行化):解决所有问题,但并发性能最差

详细说明:

脏读 :一个事务读到了另一个未提交事务修改的数据。比如事务 A 修改了某行但未提交,事务 B 读到了修改后的值,然后事务 A 回滚了,事务 B 读到的就是脏数据。READ COMMITTED 及以上级别可以避免脏读

不可重复读 :同一事务内两次读取同一行数据,结果不一致。比如事务 A 先查询某行得到值 100,事务 B 修改该行为 200 并提交,事务 A 再次查询得到 200,两次结果不同。REPEATABLE READ 及以上级别可以避免不可重复读

幻读 :同一事务内两次范围查询,结果集不一致。比如事务 A 查询 WHERE age > 18 得到 10 条记录,事务 B 插入一条 age=20 的新记录并提交,事务 A 再次查询得到 11 条记录,多出来的就是"幻影"行。InnoDB 在 REPEATABLE READ 级别通过 Next-Key Lock 锁住间隙,阻止其他事务插入满足条件的数据,从而解决幻读

READ COMMITTED 与 REPEATABLE READ 的区别 :两者都通过 MVCC 实现,区别在于 Read View 的生成时机 。RC 级别下,每次查询都生成一个新的 Read View ,所以能看到其他事务已提交的修改,导致不可重复读。RR 级别下,事务第一次查询时生成 Read View,之后一直复用,所以始终看到的是事务开始时的快照,保证了可重复读。


6. MVCC 是什么?如何实现?

核心答案:

MVCC(多版本并发控制)读不阻塞写,写不阻塞读 ,大大提高并发性能。InnoDB 通过为每行数据维护多个版本,结合 Read View 来判断哪个版本对当前事务可见。

详细说明:

InnoDB 中每行数据都有两个隐藏字段:

  • DB_TRX_ID:最近修改(插入或更新)该行的事务 ID

  • DB_ROLL_PTR:指向 undo log 中该行旧版本的指针

当更新一行数据时,InnoDB 不会直接覆盖原数据,而是:

  1. 将原数据复制到 undo log 中,形成一个旧版本

  2. 修改原数据为新值,同时更新 DB_TRX_ID 为当前事务 ID

  3. 将 DB_ROLL_PTR 指向 undo log 中的旧版本

这样就形成了一个版本链:当前行 → 上一个版本 → 更早的版本。

Read View 是事务执行快照读时生成的一个"视图",包含以下关键信息:

  • m_ids:生成 Read View 时所有活跃(未提交)的事务 ID 列表

  • min_trx_id:m_ids 中的最小值

  • max_trx_id:下一个将要分配的事务 ID

  • creator_trx_id:当前事务自己的 ID

可见性判断规则:当读取一行数据时,根据该行的 DB_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 中:

    • 在 m_ids 中,说明事务未提交,不可见

    • 不在 m_ids 中,说明事务已提交,可见

如果当前版本不可见,就通过 DB_ROLL_PTR 沿着版本链找上一个版本,重复判断直到找到可见版本或到达链尾。

不同隔离级别的 Read View 生成时机

  • READ COMMITTED每次查询都生成新的 Read View,所以能看到其他事务新提交的修改

  • REPEATABLE READ事务第一次查询时生成 Read View,之后复用,所以始终看到的是事务开始时的数据快照


7. redo log、undo log、binlog 的区别?

核心答案:

日志类型 归属 日志内容 主要作用 写入时机
redo log InnoDB 物理日志(页级别修改) 持久性、崩溃恢复 事务执行过程中持续写入
undo log InnoDB 逻辑日志(记录旧值) 原子性、MVCC 事务执行过程中持续写入
binlog Server 层 逻辑日志(SQL 语句或行记录) 主从复制、数据备份 事务提交时一次性写入

详细说明:

redo log 的详细机制

  • 采用 WAL(预写日志) 机制:先写日志,后写磁盘

  • 物理日志,记录的是"在某个数据页的某个偏移量做了什么修改",比如"在 page 100 的 offset 200 处写入值 99"

  • 循环写 :redo log 文件大小固定,有两个文件轮流写,写满一个就切换。checkpoint 指针指向已经刷盘的位置,保证循环写不会覆盖未刷盘的数据

  • 崩溃恢复时,从 checkpoint 之后开始重放 redo log,恢复所有已提交但未刷盘的事务

undo log 的详细机制

  • 逻辑日志,记录的是"如何把数据恢复回去",比如"将 id=1 的 name 从 '李四' 改回 '张三'"

  • 用于事务回滚:读取 undo log 中的旧值恢复数据

  • 用于 MVCC:当其他事务需要读取历史版本时,沿着 undo log 版本链找到可见版本

  • undo log 在事务提交后不会立即删除,因为可能还有别的事务正在读取这个旧版本。只有当事务不再需要这个版本时,purge 线程才会清理

binlog 的详细机制

  • Server 层日志,所有存储引擎都共用

  • 逻辑日志,记录的是 SQL 语句(STATEMENT 格式)或行数据变化(ROW 格式)

  • 追加写:binlog 文件写满后生成新的文件,不会覆盖

  • 主要用途主从复制 (从库拉取 binlog 进行重放)和数据恢复(基于全量备份 + binlog 恢复到任意时间点)

两阶段提交保证一致性

InnoDB 使用内部 XA 两阶段提交来保证 redo log 和 binlog 的逻辑一致:

  1. Prepare 阶段:事务执行,写入 redo log,状态为 prepare

  2. Commit 阶段:写入 binlog,然后提交事务,将 redo log 状态改为 commit

崩溃恢复时,检查 redo log 中的事务状态:

  • 如果 redo log 状态为 commit,或者 redo log 状态为 prepare 且 binlog 已写入,则提交事务

  • 如果 redo log 状态为 prepare 但 binlog 未写入,则回滚事务

这样保证了主从数据一致性:只要 binlog 写入了,redo log 就一定提交了;反过来,如果 redo log 提交了,binlog 也一定写入了。


8. 如何分析一条慢 SQL?

核心答案:

  1. 开启慢查询日志,定位具体的慢 SQL

  2. 使用 EXPLAIN 分析执行计划

  3. 根据执行计划进行针对性优化

详细说明:

第一步:开启慢查询日志

sql

复制代码
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(秒),建议设为 1
SET GLOBAL long_query_time = 1;
-- 查看慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';

第二步:使用 EXPLAIN 分析执行计划

sql

复制代码
EXPLAIN SELECT * FROM t WHERE name = '张三';

EXPLAIN 输出中需要重点关注的关键字段:

type(访问类型):从好到差排序

  • system:系统表,只有一行数据,const 的特例

  • const:通过主键或唯一索引等值查询,最多返回一行,效率最高

  • eq_ref:连接查询中,被驱动表通过主键或唯一索引访问,每个驱动行只匹配一行

  • ref:通过非唯一索引等值查询,可能返回多行

  • range:索引范围查询,如 BETWEEN、>、<、IN 等

  • index:全索引扫描,扫描整个索引树

  • ALL :全表扫描,需要重点优化

possible_keys :可能用到的索引
key :实际使用的索引,如果为 NULL,说明没走索引
key_len :使用的索引长度,可以判断联合索引用了哪几列
rows :预估需要扫描的行数,越小越好
Extra:额外信息,重点关注:

  • Using index :使用了覆盖索引,非常好

  • Using where:存储引擎返回后,Server 层再过滤,可以接受

  • Using index condition :使用了索引下推,

  • Using filesort :需要额外的排序操作,需要优化

  • Using temporary :使用了临时表,通常出现在 GROUP BY 或 DISTINCT 中,需要优化

第三步:优化方式

**1. 避免 SELECT ***

只取需要的字段,减少数据传输和回表可能。

2. 为条件字段建立合适的索引

注意联合索引的最左前缀原则

3. 避免在索引字段上使用函数或隐式类型转换

sql

复制代码
-- 错误:函数导致索引失效
SELECT * FROM t WHERE DATE(create_time) = '2024-01-01';
-- 正确:改为范围查询
SELECT * FROM t WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';

-- 错误:隐式类型转换,phone 字段是 varchar 类型
SELECT * FROM t WHERE phone = 13800000000;
-- 正确:用字符串匹配
SELECT * FROM t WHERE phone = '13800000000';

4. 优化分页查询

sql

复制代码
-- 深分页问题:offset 越大,扫描行数越多
SELECT * FROM t ORDER BY id LIMIT 100000, 10;

-- 优化方案1:使用子查询先定位起始 id
SELECT * FROM t WHERE id >= (
    SELECT id FROM t ORDER BY id LIMIT 100000, 1
) LIMIT 10;

-- 优化方案2:使用游标分页,记住上一页的 id
SELECT * FROM t WHERE id > last_id ORDER BY id LIMIT 10;

5. 使用覆盖索引

让查询的所有字段都在索引中,避免回表。

6. 合理使用 JOIN

小表驱动大表,确保 JOIN 字段有索引。


9. 什么情况会导致索引失效?

核心答案:

索引失效的常见场景:

  • 对索引字段使用函数表达式计算

  • 隐式类型转换

  • LIKE 以 % 开头(左模糊匹配)

  • 联合索引未遵循最左前缀原则

  • OR 条件中只要有一个字段无索引

  • 全表扫描比使用索引更快时(优化器选择)

详细说明:

1. 对索引字段使用函数

sql

sql 复制代码
-- 失效:对 create_time 使用了 DATE 函数
SELECT * FROM t WHERE DATE(create_time) = '2024-01-01';
-- 正确:改为范围查询,可以走索引
SELECT * FROM t WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';

2. 隐式类型转换

sql

sql 复制代码
-- 假设 phone 字段是 varchar 类型,建立了索引
-- 失效:传入的是数字,MySQL 会隐式将 phone 转为数字,相当于对字段用了 CAST 函数
SELECT * FROM t WHERE phone = 13800000000;
-- 正确:传入字符串
SELECT * FROM t WHERE phone = '13800000000';

3. LIKE 以 % 开头

sql

sql 复制代码
-- 失效:以 % 开头,无法定位索引起点
SELECT * FROM t WHERE name LIKE '%张三%';
-- 有效:以常量开头,可以走索引
SELECT * FROM t WHERE name LIKE '张三%';

4. 联合索引未遵循最左前缀

sql

sql 复制代码
-- 假设有联合索引 (a, b, c)
-- 有效:包含最左列 a
SELECT * FROM t WHERE a = 1;
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
SELECT * FROM t WHERE a = 1 AND c = 3;  -- a 有效,c 无法用(因为跳过了 b)

-- 失效:没有 a
SELECT * FROM t WHERE b = 2;
SELECT * FROM t WHERE b = 2 AND c = 3;

5. OR 条件中有无索引的字段

sql

sql 复制代码
-- 假设 a 有索引,b 无索引
-- 失效:因为 OR 需要合并结果集,b 无索引需要全表扫描,优化器可能放弃 a 的索引
SELECT * FROM t WHERE a = 1 OR b = 2;
-- 正确:将 OR 改写为 UNION
SELECT * FROM t WHERE a = 1
UNION
SELECT * FROM t WHERE b = 2;

6. 不等于操作(!= 或 <>)

不等于操作通常不走索引,因为不等于是范围查询,如果结果集太大,优化器可能选择全表扫描。

7. 优化器选择全表扫描

当表中数据量很小,或者索引选择性差(如性别字段),优化器可能认为全表扫描更快。


10. 什么是覆盖索引?什么是回表?

核心答案:

回表 :通过二级索引找到主键后,再到聚簇索引查找完整行数据的过程,需要两次 B+ 树查找

覆盖索引 :查询所需要的所有字段都在某个索引中,不需要回表,直接从索引返回数据,性能大幅提升。

详细说明:

回表的过程

text

复制代码
假设表结构:
CREATE TABLE t (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    INDEX idx_name(name)
);

执行查询:SELECT * FROM t WHERE name = '张三';

执行步骤:

  1. idx_name 索引树上查找 '张三',得到对应的主键 id 值

  2. 拿着 id 到 聚簇索引树上查找完整的行数据(包括 name、age 等所有字段)

  3. 返回结果

这就是回表,需要两次 B+ 树查找。如果查询的数据量很大,回表次数就很多,性能会下降。

覆盖索引的例子

sql

复制代码
-- 查询只需要 name 和 id,idx_name 已经包含这两个字段
SELECT id, name FROM t WHERE name = '张三';

因为 idx_name 索引的叶子节点存储的是 (name, id),查询只需要这两个字段,不需要回表 ,直接从索引返回数据。这就是覆盖索引

如何判断是否覆盖索引

在 EXPLAIN 的 Extra 字段中看到 Using index,就说明使用了覆盖索引。

覆盖索引的优化技巧

sql

sql 复制代码
-- 假设经常需要查询用户的 id 和 name 通过 phone
-- 创建覆盖索引
ALTER TABLE t ADD INDEX idx_phone_name_id(phone, name, id);
-- 或者(MySQL 5.6+ 自动包含主键)
ALTER TABLE t ADD INDEX idx_phone_name(phone, name);
-- 这样查询就可以直接从索引返回,避免回表
SELECT id, name FROM t WHERE phone = '13800000000';

第二梯队:高频进阶题(出现率 60%+)


11. 最左前缀原则是什么?

核心答案:

最左前缀原则是指:联合索引 (a, b, c) 在使用时,查询条件必须从索引的最左列开始,不能跳过中间的列。MySQL 会从左到右匹配索引列,直到遇到范围查询(>、<、BETWEEN、LIKE)为止。

详细说明:

假设有一个联合索引 (a, b, c),以下查询可以走索引:

sql

sql 复制代码
-- 使用 a 列
WHERE a = 1

-- 使用 a, b 两列
WHERE a = 1 AND b = 2

-- 使用 a, b, c 三列
WHERE a = 1 AND b = 2 AND c = 3

-- a 和 c 查询,但跳过了 b
WHERE a = 1 AND c = 3
-- 实际只有 a 用到了索引,c 无法使用(因为 b 被跳过)

以下查询无法使用索引或只能部分使用:

sql

sql 复制代码
-- 没有 a 列,完全无法使用索引
WHERE b = 2

-- 跳过了 a,无法使用索引
WHERE b = 2 AND c = 3

范围查询的影响

sql

sql 复制代码
-- 假设查询条件
WHERE a = 1 AND b > 2 AND c = 3
-- 索引使用情况:
-- a = 1 可以用索引定位
-- b > 2 可以用索引范围扫描
-- c = 3 无法使用索引,因为 b 使用了范围查询后,c 就无序了

为什么会有最左前缀原则?

B+ 树索引的排序规则是:先按 a 排序,a 相同按 b 排序,b 相同按 c 排序。所以如果查询条件中没有 a,就无法确定索引的起始位置。如果跳过 b 直接查 c,因为 b 是无序的,c 也是无序的,无法利用索引。


12. 什么是索引下推(ICP)?

核心答案:

索引下推(Index Condition Pushdown,ICP) 是 MySQL 5.6 引入的优化。它的核心是:将 where 条件中能用索引过滤的部分,下推到存储引擎层进行过滤,减少回表次数。

详细说明:

没有 ICP 之前

存储引擎层只根据索引找到数据,然后回表取出完整行,再交给 Server 层判断 where 条件。即使索引中包含 where 条件中的字段,也只能在 Server 层过滤,导致很多不必要的回表。

有 ICP 之后

存储引擎层在遍历索引时,直接在索引上判断 where 条件,只有满足条件的才回表。

举例说明

sql

复制代码
-- 假设有联合索引 (name, age)
SELECT * FROM t WHERE name LIKE '张%' AND age = 20;

没有 ICP 的执行过程

  1. 存储引擎找到所有 name 以 '张' 开头的记录(可能很多条)

  2. 对每条记录回表取完整数据

  3. 返回给 Server 层,Server 层再过滤 age = 20

  4. 问题:很多 name 符合但 age 不符合的记录也回了表,浪费 I/O

有 ICP 的执行过程

  1. 存储引擎遍历 name 索引,找到 name 以 '张' 开头的记录

  2. 在索引上直接判断 age = 20(因为 age 也在索引中)

  3. 只有 age = 20 的记录才回表取完整数据

  4. 大大减少了回表次数

如何确认是否使用了 ICP

在 EXPLAIN 的 Extra 字段中看到 Using index condition,就说明使用了索引下推。

适用条件

  • ICP 适用于二级索引

  • 需要查询的字段不全在索引中(否则就直接覆盖索引了)

  • where 条件中有一部分字段在索引中


13. 什么是 Next-Key Lock?如何解决幻读?

核心答案:

Next-Key Lock = 行锁(Record Lock)+ 间隙锁(Gap Lock) 。它锁住索引记录本身,也锁住记录之间的间隙,阻止其他事务在这个间隙中插入新数据,从而在 REPEATABLE READ 级别下解决幻读问题。

详细说明:

三种锁的类型

  • Record Lock(行锁):锁定单条索引记录

  • Gap Lock(间隙锁):锁定索引记录之间的间隙(不锁记录本身),防止插入

  • Next-Key Lock(临键锁):锁定记录本身 + 记录前的间隙

举例说明间隙锁

假设有一个表,id 列有索引,现有数据 id = 5, 10, 15, 20。

sql

复制代码
-- 事务 A 执行
SELECT * FROM t WHERE id BETWEEN 10 AND 15 FOR UPDATE;

这个查询会锁住什么?

  • Record Lock:锁住 id = 10 和 id = 15 的记录

  • Gap Lock:锁住 (5,10) 之间的间隙和 (10,15) 之间的间隙

其他事务尝试插入 id = 12 会被阻塞 ,因为 (10,15) 间隙被锁住了。这样就防止了幻读:事务 A 两次查询 BETWEEN 10 AND 15 结果集不会变化。

Next-Key Lock 的退化规则

  • 唯一索引等值查询且记录存在:Next-Key Lock 退化为 Record Lock(只锁记录本身)

  • 唯一索引等值查询且记录不存在:Next-Key Lock 退化为 Gap Lock(只锁间隙)

  • 普通索引或范围查询:使用 Next-Key Lock

为什么唯一索引等值查询不需要间隙锁?

因为唯一索引保证值唯一,其他事务不可能插入相同值的数据,所以不需要锁间隙防止幻读。


14. 死锁是如何发生的?如何排查和避免?

核心答案:

死锁是两个或多个事务互相持有对方需要的锁,并且互相等待,导致都无法继续执行。

详细说明:

死锁发生的四个必要条件

  1. 互斥:资源只能被一个事务持有

  2. 持有并等待:事务持有锁的同时等待其他锁

  3. 不可剥夺:已持有的锁不能被其他事务强行剥夺

  4. 循环等待:事务之间形成等待环

举例

text

复制代码
事务 A:UPDATE t SET age=20 WHERE id=1;  -- 持有 id=1 的行锁
事务 B:UPDATE t SET age=30 WHERE id=2;  -- 持有 id=2 的行锁

事务 A:UPDATE t SET age=25 WHERE id=2;  -- 等待事务 B 释放 id=2
事务 B:UPDATE t SET age=35 WHERE id=1;  -- 等待事务 A 释放 id=1

-- 形成死锁

排查死锁的方法

  1. 查看最近一次死锁信息

sql

复制代码
SHOW ENGINE INNODB STATUS;

重点关注 LATEST DETECTED DEADLOCK 部分,可以看到:

  • 哪些事务参与了死锁

  • 每个事务持有的锁和等待的锁

  • MySQL 选择哪个事务作为"牺牲品"回滚

  1. 查看当前正在运行的事务

sql

复制代码
SELECT * FROM information_schema.INNODB_TRX\G

查看 trx_statetrx_startedtrx_mysql_thread_id 等字段。

  1. 查看当前锁信息

sql

复制代码
SELECT * FROM performance_schema.data_locks;

避免死锁的方法

  1. 统一访问顺序

    让所有事务按相同的顺序访问资源,避免循环等待。

  2. 保持事务简短

    长事务持有锁的时间长,增加死锁概率。及时提交事务。

  3. 使用较低的隔离级别

    比如 READ COMMITTED,减少间隙锁的使用。

  4. 合理设计索引

    索引选择性好,锁定的行数就少,降低冲突概率。

  5. 使用 SELECT ... FOR UPDATE 时注意顺序

    主动加锁时,确保按相同顺序加锁。

  6. 使用 INSERT ... ON DUPLICATE KEY UPDATE 时注意

    这种语句会加 Next-Key Lock,可能造成死锁。

MySQL 的死锁处理

MySQL 会自动检测死锁 ,选择一个事务作为"牺牲品"回滚(通常是更新行数较少的事务),另一个事务继续执行。应用层需要重试机制来处理被回滚的事务。


15. binlog 的三种格式有什么区别?

核心答案:

binlog 有三种格式:

  • STATEMENT:记录原始 SQL 语句

  • ROW:记录每行数据的变化(前镜像和后镜像)

  • MIXED:MySQL 自动选择,STATEMENT 可能不安全时用 ROW

详细说明:

STATEMENT 格式

  • 记录的是执行的 SQL 语句

  • 优点:日志量小,节省磁盘空间和网络传输

  • 缺点:可能造成主从数据不一致。比如 NOW()UUID()RAND() 等非确定性函数,在主库执行时得到一个值,从库重放时又得到另一个值,导致数据不一致

ROW 格式

  • 记录的是每行数据的变化,格式为"修改前数据 + 修改后数据"

  • 优点:最安全,保证主从数据完全一致。即使是非确定性函数也能正确复制

  • 缺点:日志量大,尤其是批量更新时,每条记录的变化都会记录

  • 额外优势:可以通过 binlog 进行数据恢复,能精确到每一行的变化

MIXED 格式

  • MySQL 自动判断:普通 SQL 用 STATEMENT,如果包含非确定性函数或可能不安全(如 LIMIT 不带 ORDER BY)时自动切换为 ROW

  • 优点:兼顾 STATEMENT 的日志量小和 ROW 的安全性

生产环境建议

  • 如果对数据一致性要求高(大多数业务场景),推荐使用 ROW 格式

  • MySQL 8.0 默认使用 ROW 格式

如何查看和设置 binlog 格式

sql

复制代码
-- 查看当前格式
SHOW VARIABLES LIKE 'binlog_format';

-- 设置格式(需要重启或动态设置)
SET GLOBAL binlog_format = 'ROW';

16. 主从复制的原理是什么?有哪些复制方式?

核心答案:

主从复制的核心是:主库将数据变更写入 binlog,从库拉取 binlog 并重放

详细说明:

复制架构

text

复制代码
主库(Master)         从库(Slave)
    │                       │
    │   binlog              │
    │   ───────────────────>│ I/O 线程
    │                       │   ↓
    │                   relay log
    │                       │
    │                       │ SQL 线程
    │                       │   ↓
    │                   数据文件

三个线程的工作

  1. 主库的 binlog dump 线程:当从库连接时,主库启动这个线程,负责读取 binlog 并发送给从库

  2. 从库的 I/O 线程:连接主库,拉取 binlog 并写入本地的 relay log(中继日志)

  3. 从库的 SQL 线程:读取 relay log 并重放(执行 SQL),把数据写入从库

三种复制方式

1. 异步复制(默认)

  • 主库提交事务后,不等待从库确认,直接返回客户端成功

  • 优点:性能最好,对主库无影响

  • 缺点:如果主库崩溃时从库还没收到 binlog,数据可能丢失

2. 半同步复制

  • 主库提交事务后,等待至少一个从库确认收到 binlog 后才返回客户端成功

  • 优点:数据安全性更高,至少有一个从库有完整数据

  • 缺点:性能略低于异步复制,如果从库延迟或故障,主库会等待(可设置超时时间,超时后降级为异步)

  • 需要安装插件:rpl_semi_sync_masterrpl_semi_sync_slave

3. 组复制(Group Replication)

  • MySQL 5.7 引入,基于 Paxos 协议,多主或多单主模式

  • 优点:高可用性、自动故障切换、数据强一致性

  • 缺点:配置复杂,网络要求高

并行复制

MySQL 5.6 开始支持并行复制,从库可以用多个 SQL 线程并行重放 relay log,减少主从延迟。相关参数:

  • slave_parallel_workers:并行线程数

  • slave_parallel_typeLOGICAL_CLOCK(基于事务分组)或 DATABASE(基于数据库)


17. 什么是主从延迟?如何解决?

核心答案:

主从延迟 是指从库的数据落后于主库的时间差,通常表现为 Seconds_Behind_Master 大于 0。主要原因是从库单线程重放跟不上主库的写入速度。

详细说明:

查看主从延迟

sql

复制代码
SHOW SLAVE STATUS\G
-- 关注字段:
-- Seconds_Behind_Master:延迟秒数
-- Relay_Log_File / Relay_Log_Pos:当前重放位置
-- Master_Log_File / Read_Master_Log_Pos:已拉取的位置

常见原因

  1. 大事务:一个事务在从库重放耗时很长,期间无法处理其他事务

  2. 从库单线程重放:虽然主库可以并行写入,但从库 SQL 线程早期是单线程的

  3. 从库硬件差:从库配置低于主库,执行 SQL 更慢

  4. 锁竞争:从库重放时的锁等待

  5. 从库有其他查询负载:如报表查询占用从库资源

解决方案

1. 开启并行复制

sql

复制代码
-- MySQL 5.7+ 设置
SET GLOBAL slave_parallel_workers = 8;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';

2. 拆解大事务

  • 将大批量更新拆分为多个小事务

  • 比如一次删除 100 万行,改为每次删除 1 万行,循环执行

3. 优化从库硬件

  • 从库使用 SSD

  • 调整从库的 innodb_flush_log_at_trx_commit 参数

4. 使用半同步复制

  • 保证主从数据实时性,但会略微影响主库性能

5. 读写分离策略

  • 对延迟敏感的业务读主库

  • 对延迟不敏感的业务(如报表)读从库

6. 使用组复制或 MGR

  • 提供更强的一致性保证

18. 什么是分库分表?有哪些策略?

核心答案:

分库分表是当单库单表数据量过大、并发过高时,将数据分散到多个库和多个表中的技术方案。

详细说明:

分库分表的两种类型

1. 垂直分库/分表

  • 垂直分表:将一张宽表按字段拆分为多张表,比如把不常访问的大字段(如 text、blob)拆出去

  • 垂直分库:按业务模块拆分到不同数据库,如订单库、用户库、商品库

2. 水平分库/分表

  • 水平分表:将同一张表的数据按规则分散到多张结构相同的表中

  • 水平分库:将数据分散到多个数据库实例中

常用分片策略

策略 说明 优点 缺点
取模分片 shard_key % 库/表数量 数据分布均匀 扩容时数据需要迁移
范围分片 按时间、ID 范围划分 扩容简单,只需新增分片 可能产生热点(如近期数据)
一致性哈希 哈希环算法 扩容时数据迁移量小 实现复杂
哈希 + 范围 先哈希,再按范围 兼顾均匀和扩展性 复杂度高

分库分表带来的问题

  1. 跨分片 JOIN:无法直接 JOIN,需要在应用层聚合或使用全局表(广播表)

  2. 分布式事务:跨分片的事务需要 XA 或最终一致性方案

  3. 全局唯一 ID:不能使用自增主键,需要分布式 ID 生成器(如雪花算法、美团 Leaf)

  4. 分页查询:跨分片的排序分页需要在中间件层聚合,性能差

  5. 扩容困难:取模分片扩容需要数据迁移

常用中间件

  • ShardingSphere:Apache 开源,支持 Java 应用层分片

  • MyCat:基于 Proxy 的数据库中间件

  • Vitess:YouTube 开源,支持大规模集群


19. 什么是分布式 ID?有哪些生成方案?

核心答案:

分布式 ID 是在分布式系统中生成全局唯一标识符的方案。在分库分表场景下,数据库自增 ID 无法保证全局唯一,需要独立的 ID 生成器。

详细说明:

分布式 ID 的要求

  • 全局唯一:不能重复

  • 趋势递增:便于数据库索引(B+ 树插入效率)

  • 高性能:生成 ID 的 QPS 要高

  • 高可用:不能成为系统瓶颈

常用方案对比

1. UUID

  • 格式:32 位十六进制数,如 550e8400-e29b-41d4-a716-446655440000

  • 优点:本地生成,性能高,实现简单

  • 缺点:无序 ,作为主键会导致 B+ 树频繁页分裂;太长,占用存储空间大

2. 数据库自增 ID 单独表

  • 创建一张专门用来生成 ID 的表,使用 REPLACE INTOINSERT ... ON DUPLICATE KEY UPDATE 获取自增 ID

  • 优点:实现简单,ID 趋势递增

  • 缺点:单点故障风险,性能受限于数据库

3. 数据库号段模式

  • 预先分配一批 ID 到应用内存,用完后再取新号段

  • 优点:减少数据库访问,性能高

  • 缺点:应用重启可能浪费未用完的 ID

4. Redis 生成 ID

  • 使用 INCRINCRBY 命令

  • 优点:性能高,可以控制步长

  • 缺点:Redis 持久化可能丢失 ID,需要额外的维护成本

5. 雪花算法(Snowflake)

  • Twitter 开源的分布式 ID 生成算法,64 位整数,结构如下:

    • 1 位:符号位,0 表示正数

    • 41 位:时间戳(毫秒级),可以支持 69 年

    • 10 位:机器 ID(5 位数据中心 + 5 位机器)

    • 12 位:序列号,同一毫秒内最多 4096 个 ID

  • 优点:趋势递增,性能高(单机每秒可达数十万),不依赖外部服务

  • 缺点:依赖机器时钟,时钟回拨会导致 ID 重复

雪花算法时钟回拨解决方案

  • 等待时钟追上

  • 预留序列号空间

  • 使用时钟回拨检测,如果回拨小于阈值则等待,大于阈值则报错

6. 美团 Leaf

  • 基于雪花算法和号段模式的双方案

  • 提供高可用、高可靠的分布式 ID 服务

  • 支持通过 ZooKeeper 管理机器 ID


20. CHAR 和 VARCHAR 的区别?

核心答案:

对比项 CHAR VARCHAR
存储方式 定长,固定分配空间 变长,按实际长度存储
最大长度 255 字节 65535 字节
存储开销 无长度前缀 有 1-2 字节长度前缀
空间效率 浪费空间 节省空间
性能 读取快 写入有额外开销

详细说明:

CHAR(n)

  • 固定长度,无论实际存储多少字符,都分配 n 个字符的空间

  • 存储时会删除末尾空格,读取时会补上空格

  • 适合存储长度固定的数据,如手机号、MD5 值、状态码等

VARCHAR(n)

  • 可变长度,按实际存储的字符数分配空间

  • 需要额外 1-2 字节存储长度信息(n <= 255 时 1 字节,否则 2 字节)

  • 适合存储长度变化较大的数据,如用户名、地址、描述等

选择建议

  • 长度固定或变化很小:用 CHAR(如手机号、身份证号)

  • 长度变化大:用 VARCHAR

  • VARCHAR(255) 是一个常用值,因为 255 以内长度前缀只需 1 字节


21. COUNT(*) 和 COUNT(1) 和 COUNT(列) 的区别?

核心答案:

COUNT(*)COUNT(1) 性能相同,都统计所有行数。COUNT(列) 统计该列非 NULL 值的行数。

详细说明:

  • COUNT(*):统计所有行的数量,包括 NULL 值。优化器会优先选择最小的非聚簇索引来统计,如果没有索引就全表扫描。

  • COUNT(1):与 COUNT(*) 完全等价,优化器会做同样的优化,没有性能差异。

  • COUNT(列):统计指定列非 NULL 值的数量。如果该列有 NOT NULL 约束,优化器可以转换为 COUNT(*),否则需要逐行判断是否为 NULL。

常见误解

  • 有人说 COUNT(1) 比 COUNT(*) 快,这是错误的。MySQL 优化器已经做了优化,两者完全相同。

  • 有人说 COUNT(主键) 最快,实际上优化器可能选择更小的二级索引,不一定选择主键。

性能排序(有 NOT NULL 约束时)

COUNT(列) 如果列有索引且 NOT NULL,性能与 COUNT(*) 接近。

性能排序(可能包含 NULL 时)

COUNT(*) ≈ COUNT(1) > COUNT(主键) > COUNT(有索引列) > COUNT(无索引列)


22. 什么是 SQL 注入?如何防范?

核心答案:

SQL 注入是攻击者通过构造恶意的 SQL 语句,欺骗数据库执行非预期的 SQL 命令,从而获取敏感数据或破坏数据库的安全漏洞。

详细说明:

SQL 注入的原理

java

复制代码
// 错误写法:直接拼接用户输入
String sql = "SELECT * FROM users WHERE name = '" + userName + "'";
// 如果 userName 输入:' OR '1'='1
// 最终执行的 SQL:SELECT * FROM users WHERE name = '' OR '1'='1'
// 这会返回所有用户数据

防范方法

1. 使用预编译语句(PreparedStatement)

java

复制代码
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, userName);
// 参数会被安全转义,不会作为 SQL 代码执行

2. 使用 ORM 框架

MyBatis、Hibernate 等框架默认使用预编译,注意 MyBatis 中 #{} 是预编译,${} 是字符串拼接,尽量避免使用 ${}

3. 严格校验输入

对用户输入进行白名单校验,如枚举值、正则表达式等。

4. 最小权限原则

数据库账号只授予必要的权限,不使用 root 账号连接应用。

5. 使用 Web 应用防火墙

检测和拦截 SQL 注入攻击。

6. 隐藏数据库错误信息

生产环境不暴露详细的数据库错误信息,避免给攻击者提供线索。


23. UNION 和 UNION ALL 的区别?

核心答案:

UNION 会对结果集进行去重UNION ALL 保留所有结果(包括重复行)。UNION ALL 性能更好,因为不需要去重操作。

详细说明:

sql

复制代码
-- UNION:会去重,有额外排序开销
SELECT id FROM t1 UNION SELECT id FROM t2;

-- UNION ALL:不去重,直接拼接结果
SELECT id FROM t1 UNION ALL SELECT id FROM t2;

使用建议

  • 如果确定结果集不会重复,或者可以接受重复,使用 UNION ALL

  • 去重操作需要排序或哈希,消耗 CPU 和内存

  • UNION ALL 不会产生临时表(某些情况),执行效率更高


24. EXISTS 和 IN 的区别?哪个性能更好?

核心答案:

EXISTSIN 都可以用于子查询,但执行逻辑不同。性能取决于子查询结果集大小外层查询大小

详细说明:

IN 的执行逻辑

sql

复制代码
SELECT * FROM t1 WHERE id IN (SELECT id FROM t2);

先执行子查询,将结果集(通常是 t2 的 id 列表)物化,然后遍历 t1 判断是否在结果集中。适合子查询结果集较小的场景。

EXISTS 的执行逻辑

sql

复制代码
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id);

遍历 t1 的每一行,执行子查询判断是否存在匹配。适合外层结果集较小子查询有索引的场景。

性能选择原则

  • IN 优化:子查询结果集小,可以用 IN

  • EXISTS 优化:外层结果集小,子查询大,用 EXISTS

  • MySQL 优化器会做优化,但在复杂场景下需要手动选择

注意

  • 子查询中的 SELECT 1SELECT * 不影响结果,EXISTS 只关心是否存在匹配行

  • 现代 MySQL 版本中,IN 和 EXISTS 在某些情况下会被优化器重写为相同的执行计划


25. 什么是三大范式?为什么要遵守?

核心答案:

数据库三大范式 是规范数据库表设计的准则,目的是减少数据冗余避免数据不一致

详细说明:

第一范式(1NF):原子性

  • 要求:列不可再分,即每个字段都是原子值

  • 反例:phone_numbers 字段存储 138****,139**** 多个号码

  • 正例:拆分为多行或多列

第二范式(2NF):完全依赖

  • 要求:在 1NF 基础上,非主键列必须完全依赖主键(不能部分依赖)

  • 场景:联合主键表中,如果某列只依赖主键的一部分,就违反 2NF

  • 解决:拆分成多张表

第三范式(3NF):无传递依赖

  • 要求:在 2NF 基础上,非主键列不能传递依赖主键

  • 反例:订单表中有 user_iduser_name,user_name 依赖 user_id,不直接依赖订单 id

  • 解决:将用户信息拆分到用户表

是否必须严格遵守?

  • 生产环境不一定要完全遵守 ,有时会反范式设计以提升性能

  • 适当冗余可以减少 JOIN 查询,提高读性能

  • 需要在冗余带来的性能提升维护成本增加之间权衡

第三梯队:深度进阶题(出现率 30%+)


26. 什么是自适应哈希索引?

核心答案:

自适应哈希索引(Adaptive Hash Index,AHI) 是 InnoDB 的一个优化特性,它会根据查询频率 ,自动将热点索引页的某些值构建成哈希索引,实现 O(1) 级别的单点查询

详细说明:

工作原理

  • InnoDB 监控对 B+ 树索引的访问

  • 如果发现某个页被频繁访问,并且满足一定条件(如同一个值被多次查询),就为该页构建哈希索引

  • 哈希索引只存在于内存中,不持久化

  • 下次查询可以直接通过哈希查找,避免 B+ 树遍历

优点

  • 热点数据的等值查询性能大幅提升

  • 对业务透明,自动生效

缺点

  • 消耗额外的内存

  • 对写入性能有一定影响(维护哈希索引有开销)

查看状态

sql

复制代码
SHOW ENGINE INNODU STATUS\G
-- 查看 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分

控制参数

sql

复制代码
-- 关闭自适应哈希索引
SET GLOBAL innodb_adaptive_hash_index = OFF;

27. 什么是 change buffer?有什么作用?

核心答案:

change buffer 是 InnoDB 的一个优化机制,用于缓存对非唯一二级索引页的修改操作,减少随机 I/O,提升写入性能。

详细说明

工作原理

  • 当更新一个非唯一二级索引时,如果目标索引页不在内存中,InnoDB 不会立即从磁盘读取该页

  • 而是将修改操作缓存到 change buffer 中

  • 后续当该索引页被读取到内存时,再将 change buffer 中的修改合并(称为 merge

适用场景

  • 只适用于非唯一二级索引

  • 唯一索引需要立即检查唯一性,不能缓存

  • 写多读少的场景效果最明显

优点

  • 减少随机 I/O:将多次随机写合并为一次

  • 提升写入性能:批量写入

控制参数

sql

复制代码
-- 查看 change buffer 大小
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
-- 默认 25,表示最大占用 buffer pool 的 25%

监控

sql

复制代码
SHOW ENGINE INNODU STATUS\G
-- 查看 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分
-- merged operations:合并次数
-- inserts:插入操作数

28. 什么是 doublewrite buffer?为什么需要?

核心答案:

doublewrite buffer(双写缓冲区) 是 InnoDB 保证数据页完整性的机制,用于解决部分写失效问题。

详细说明

部分写失效

  • 磁盘写入的最小单位是扇区(512 字节),而 InnoDB 的数据页是 16KB

  • 如果写数据页时发生断电或系统崩溃,可能只写入了部分数据(如只写 4KB)

  • 导致数据页损坏,redo log 也无法恢复(redo log 记录的是页内修改,依赖页的完整性)

doublewrite buffer 的工作流程

  1. 当 InnoDB 需要将脏页刷盘时,先写入 doublewrite buffer(连续写,顺序 I/O)

  2. doublewrite buffer 写入成功后,再写入数据文件的实际位置(随机 I/O)

  3. 崩溃恢复时,如果发现数据页损坏,从 doublewrite buffer 中恢复

doublewrite buffer 的位置

  • 位于系统表空间(ibdata 文件)中,共 2MB,分为 2 个区(每个 1MB)

  • 每次写入 128 个页(128 × 16KB = 2MB)

为什么能解决部分写

  • 写入 doublewrite buffer 时是顺序写,即使崩溃也不影响已写入的完整页

  • 恢复时检查数据页的 checksum,如果校验失败,从 doublewrite buffer 中拷贝正确版本

性能影响

  • 每次脏页刷盘需要写两次:一次 doublewrite,一次实际位置

  • 但 doublewrite 是顺序写,开销相对较小

  • SSD 场景下,可以通过 skip_innodb_doublewrite 关闭(有风险)


29. 什么是页分裂?如何避免?

核心答案:

页分裂 是当 InnoDB 的 B+ 树索引页写满后,需要插入新数据时,将一页拆分为两页的过程。自增主键可以避免页分裂。

详细说明

页分裂的过程

  • InnoDB 数据页默认 16KB,当页写满后,新插入数据需要分配新页

  • 将原页的一部分数据移动到新页,并调整父节点指针

  • 这是一个开销较大的操作,需要移动数据、更新指针

页分裂的影响

  • 性能下降:需要额外 I/O 和 CPU

  • 空间浪费:新页可能未写满,产生碎片

  • 索引维护成本高

如何避免

  • 使用自增主键:新数据总是插入到当前页末尾,页满时分配新页追加,不会触发分裂

  • 避免使用 UUID 或随机值作为主键:插入位置随机,频繁导致页分裂

  • 合理设置 fill_factor(填充因子):保留一定空间

举例

text

复制代码
自增主键插入:1,2,3,4,5... 始终在最后,页满就开新页
UUID 插入:随机位置,可能插入到中间页,导致页分裂

30. 什么是脏读、不可重复读、幻读?区别是什么?

核心答案:

这三种是并发事务可能产生的数据不一致问题,严重程度递增,不同隔离级别解决不同问题。

详细说明

问题 定义 产生条件 解决方案
脏读 读到未提交的数据 事务 A 修改但未提交,事务 B 读取 隔离级别 ≥ RC
不可重复读 同一事务两次读同一行结果不同 事务 B 在事务 A 两次查询之间修改并提交了数据 隔离级别 ≥ RR
幻读 同一事务两次范围查询结果集不同 事务 B 在事务 A 两次查询之间插入或删除了数据 隔离级别 = Serializable 或 RR + Next-Key Lock

举例说明

脏读

text

复制代码
事务 A:UPDATE 账户 SET 余额 = 余额 - 100 WHERE id = 1;(未提交)
事务 B:SELECT 余额 FROM 账户 WHERE id = 1; -- 读到减少后的金额
事务 A:ROLLBACK; -- 回滚,事务 B 读到的就是脏数据

不可重复读

text

复制代码
事务 A:SELECT 余额 FROM 账户 WHERE id = 1; -- 得到 1000
事务 B:UPDATE 账户 SET 余额 = 900 WHERE id = 1; COMMIT;
事务 A:SELECT 余额 FROM 账户 WHERE id = 1; -- 得到 900,两次结果不同

幻读

text

复制代码
事务 A:SELECT COUNT(*) FROM 账户 WHERE 余额 > 500; -- 得到 5 条
事务 B:INSERT INTO 账户 (余额) VALUES (600); COMMIT;
事务 A:SELECT COUNT(*) FROM 账户 WHERE 余额 > 500; -- 得到 6 条,多出幻影行

31. 一条 UPDATE 语句的执行流程是怎样的?

核心答案

一条 UPDATE 语句在 MySQL 中要经过连接器、分析器、优化器、执行器、存储引擎 等多个组件,涉及 undo log、redo log、binlog 的写入。

详细说明

text

复制代码
UPDATE t SET age = 20 WHERE id = 1;

执行流程

  1. 连接器:验证用户名密码,获取权限

  2. 分析器:词法分析识别表名、字段,语法分析检查 SQL 语法

  3. 优化器:选择最优执行计划,决定使用哪个索引(id 主键索引)

  4. 执行器:调用 InnoDB 引擎接口

InnoDB 内部流程

  1. 根据 id=1 在主键索引树上找到该行数据(如果不在内存,先从磁盘读入 buffer pool)

  2. 记录 undo log:将修改前的数据写入 undo log(用于回滚和 MVCC)

  3. 修改内存数据:将 buffer pool 中的数据改为 age = 20,标记为脏页

  4. 写入 redo log(prepare 状态):将修改记录到 redo log buffer,状态为 prepare

  5. 写入 binlog:将 SQL 或行变化写入 binlog

  6. 提交事务:将 redo log 状态改为 commit,释放锁

两阶段提交

  • 第 8 步写入 redo log prepare

  • 第 9 步写入 binlog

  • 第 10 步 redo log commit

  • 保证 redo log 和 binlog 逻辑一致

脏页刷盘

  • 后台线程定期将 buffer pool 中的脏页刷回磁盘

  • 时机:redo log 写满、buffer pool 空间不足、MySQL 空闲时、MySQL 关闭时


32. 什么是 buffer pool?如何调优?

核心答案

buffer pool 是 InnoDB 的内存缓冲池,用于缓存数据页和索引页,减少磁盘 I/O,是 InnoDB 性能的核心。

详细说明

buffer pool 的作用

  • 缓存数据页和索引页

  • 缓存 change buffer

  • 缓存自适应哈希索引

  • 缓存行锁信息

关键参数

sql

复制代码
-- buffer pool 大小(建议设置为物理内存的 70%-80%)
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- buffer pool 实例数(多实例减少锁竞争)
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';

调优建议

  1. 合理设置大小

    • 专用数据库服务器:物理内存的 70%-80%

    • 混合部署:根据实际情况调整,留足系统内存

  2. 开启多实例

    • 当 buffer pool >= 8GB 时,设置 innodb_buffer_pool_instances = 8

    • 减少内部锁竞争

  3. 预热 buffer pool

    sql

    复制代码
    -- 保存当前 buffer pool 状态
    SET GLOBAL innodb_buffer_pool_dump_now = ON;
    -- 重启后加载
    SET GLOBAL innodb_buffer_pool_load_now = ON;
  4. 监控命中率

    sql

    复制代码
    SHOW ENGINE INNODU STATUS\G
    -- 查看 Buffer pool hit rate
    -- 命中率应该 > 99%

33. 什么是 MySQL 的查询缓存?为什么被移除?

核心答案

查询缓存 是 MySQL 5.7 及之前版本的一个特性,用于缓存 SELECT 语句的结果集。在 MySQL 8.0 中被彻底移除

详细说明

工作原理

  • 当执行 SELECT 查询时,MySQL 将 SQL 语句作为 key,结果集作为 value 存入内存

  • 再次执行相同 SQL 时,直接从缓存返回结果,跳过解析、优化、执行等步骤

为什么被移除

  1. 失效太频繁:只要表数据发生变化(INSERT、UPDATE、DELETE),该表的所有查询缓存都失效。对于写多读少的场景,缓存几乎无效

  2. 锁竞争严重:查询缓存有全局锁,多个查询同时访问时会竞争

  3. 缓存粒度粗:以 SQL 语句为粒度,多一个空格、大小写不同都不命中

  4. 维护成本高:缓存失效检查、内存管理增加复杂度

替代方案

  • 使用 Redis 等外部缓存,更灵活、可控

  • 应用层自己做缓存,可以精细控制失效策略


34. 什么是 online DDL?如何减少 DDL 对业务的影响?

核心答案

online DDL 是 MySQL 5.6 引入的特性,允许在执行 DDL 操作(如 ALTER TABLE)时,不阻塞并发的 DML 操作,减少对业务的影响。

详细说明

online DDL 的三种类型

操作类型 是否允许并发 DML 是否需要重建表
INSTANT 允许,瞬间完成 否(MySQL 8.0+)
INPLACE 允许 是(原地重建)
COPY 不允许(阻塞) 是(复制表)

常用 online DDL 操作

sql

复制代码
-- 添加索引(INPLACE,允许并发 DML)
ALTER TABLE t ADD INDEX idx_name (name), ALGORITHM=INPLACE, LOCK=NONE;

-- 添加列(MySQL 8.0 支持 INSTANT)
ALTER TABLE t ADD COLUMN age INT, ALGORITHM=INSTANT;

-- 修改列类型(通常需要 COPY,会锁表)
ALTER TABLE t MODIFY COLUMN name VARCHAR(200), ALGORITHM=COPY;

减少影响的策略

  1. 使用 online DDL :指定 ALGORITHM=INPLACELOCK=NONE

  2. 使用 pt-online-schema-change:Percona 工具,通过触发器实现无锁 DDL

  3. 在业务低峰期执行:如凌晨 2-4 点

  4. 分批执行:大表操作拆分为多个小操作

  5. 监控 DDL 进度

    sql

    复制代码
    -- MySQL 5.7+ 查看 DDL 进度
    SELECT * FROM performance_schema.events_stages_current;

35. 什么是临时表?什么情况下会使用临时表?

核心答案

临时表 是 MySQL 在执行某些 SQL 时,由于无法直接返回结果,需要在内存或磁盘中创建临时表来存储中间结果。

详细说明

临时表的类型

  • 内存临时表:使用 MEMORY 引擎,数据量小时优先使用

  • 磁盘临时表 :使用 InnoDB 或 MyISAM,当内存临时表超过 tmp_table_sizemax_heap_table_size 时转换为磁盘临时表

什么情况会使用临时表

  1. GROUP BY 没有使用索引

  2. DISTINCT 无法使用索引去重

  3. UNION(不是 UNION ALL)需要去重

  4. ORDER BYGROUP BY 的列不同

  5. 子查询的物化

  6. 多表 JOIN 时某些特殊情况

如何识别

在 EXPLAIN 的 Extra 字段中看到 Using temporary,说明使用了临时表。

优化方法

  • 为 GROUP BY、ORDER BY 的字段建立合适的索引

  • 使用 UNION ALL 代替 UNION(如果不需要去重)

  • 增加 tmp_table_sizemax_heap_table_size 的值

  • 优化 SQL 结构,避免复杂的聚合


36. 什么是慢查询日志?如何分析?

核心答案

慢查询日志 是 MySQL 记录执行时间超过指定阈值的 SQL 语句的日志,是性能优化的首要工具。

详细说明

配置慢查询日志

sql

复制代码
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
-- 设置慢查询阈值(秒)
SET GLOBAL long_query_time = 1;
-- 设置日志文件路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 是否记录未使用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;

分析工具

  1. mysqldumpslow(MySQL 自带):

    bash

    复制代码
    # 按平均查询时间排序,取前 10 条
    mysqldumpslow -s at -t 10 /var/log/mysql/slow.log
  2. pt-query-digest(Percona Toolkit):

    bash

    复制代码
    # 生成详细分析报告
    pt-query-digest /var/log/mysql/slow.log > slow_report.txt

分析维度

  • Query_time:执行时间,越长越需要优化

  • Lock_time:锁等待时间

  • Rows_examined:扫描行数,应尽量小

  • Rows_sent:返回行数,与 Rows_examined 对比判断索引效率


37. 什么是执行计划?如何读懂?

核心答案

执行计划 是 MySQL 优化器根据 SQL 语句生成的具体执行方案 ,通过 EXPLAIN 命令查看,是优化 SQL 的核心依据。

详细说明

EXPLAIN 输出关键字段

字段 含义 重点关注
id 执行顺序 id 越大越先执行,相同则从上到下
select_type 查询类型 SIMPLE(简单查询)、PRIMARY(外层)、SUBQUERY(子查询)、DERIVED(派生表)、UNION 等
table 访问的表 -
type 访问类型 system > const > eq_ref > ref > range > index > ALL,越靠左越好
possible_keys 可能使用的索引 -
key 实际使用的索引 NULL 表示没走索引
key_len 索引长度 可以判断联合索引用了哪几列
ref 索引列与什么比较 const(常量)、字段名等
rows 预估扫描行数 越小越好
filtered 过滤比例 越高越好
Extra 额外信息 Using index (覆盖索引)、Using whereUsing temporary (需优化)、Using filesort (需优化)、Using index condition(索引下推)

示例

sql

复制代码
EXPLAIN SELECT * FROM t WHERE name = '张三'\G

type 详解

  • system:系统表,只有一行

  • const:主键或唯一索引等值查询

  • eq_ref:连接查询,被驱动表通过主键/唯一索引访问

  • ref:非唯一索引等值查询

  • range:索引范围查询

  • index:全索引扫描

  • ALL :全表扫描,最差


38. 什么是数据库连接池?为什么需要?

核心答案

数据库连接池是维护一组数据库连接的缓存池,应用程序从池中获取连接,使用后归还,而不是每次操作都新建和关闭连接。

详细说明

为什么需要连接池

  • 建立数据库连接是耗时操作(TCP 三次握手、认证、权限检查等)

  • 频繁创建和关闭连接会消耗大量系统资源

  • 连接池复用连接,大幅提升性能

常用连接池

  • HikariCP:Spring Boot 默认,性能最好

  • Druid:阿里开源,功能丰富(监控、SQL 防火墙)

  • Tomcat JDBC Pool:Tomcat 内置

  • c3p0:老牌连接池

关键参数

yaml

复制代码
# HikariCP 示例
maximumPoolSize: 20      # 最大连接数
minimumIdle: 10          # 最小空闲连接数
connectionTimeout: 30000 # 获取连接超时时间(ms)
idleTimeout: 600000      # 空闲连接存活时间
maxLifetime: 1800000     # 连接最大生命周期

调优建议

  • 最大连接数根据数据库 CPU 核数、并发量设置(通常 10-50)

  • 最小空闲连接数根据业务波动设置

  • 监控连接池使用率,避免连接数不足或过多


39. 什么是读写分离?如何实现?

核心答案

读写分离 是将数据库的写操作(INSERT、UPDATE、DELETE)指向主库读操作(SELECT)指向从库,分摊主库压力,提升系统吞吐量。

详细说明

架构

text

复制代码
         ┌─────────┐
         │ 应用层  │
         └────┬────┘
              │
         ┌────▼────┐
         │ 中间件  │(可选)
         └────┬────┘
        ┌─────┴─────┐
        │           │
    ┌───▼───┐   ┌───▼───┐
    │ 主库  │   │ 从库  │
    │ 写入  │   │ 读取  │
    └───────┘   └───────┘

实现方式

  1. 应用层实现

    • 在代码中区分数据源,写用主库,读用从库

    • Spring 中可以使用 AbstractRoutingDataSource 动态路由

    • 优点:灵活可控

    • 缺点:侵入代码

  2. 中间件实现

    • ShardingSphere-JDBC:轻量级 Java 组件,支持读写分离

    • MyCat:代理层,对应用透明

    • ProxySQL:高性能 MySQL 代理

    • 优点:对应用透明

    • 缺点:增加架构复杂度

  3. MySQL Router

    • MySQL 官方路由,轻量级

注意事项

  • 主从延迟:刚写入的数据可能读不到,需要业务容忍或强制读主库

  • 事务一致性:事务内的读操作应该走主库,避免读到不一致数据

  • 负载均衡:从库多时,需要负载均衡策略


40. 什么是分库分表的垂直拆分和水平拆分?

核心答案

  • 垂直拆分 :按业务模块字段拆分,解决单表列太多或不同业务耦合的问题

  • 水平拆分 :按分片键将数据分散到多个表/库,解决单表数据量过大的问题

详细说明

垂直拆分示例

垂直分表

sql

复制代码
-- 原表:用户表(列太多)
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    password VARCHAR(100),
    bio TEXT,           -- 大字段,不常访问
    avatar BLOB         -- 大字段
);

-- 拆分为:
CREATE TABLE user_base (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    password VARCHAR(100)
);

CREATE TABLE user_ext (
    id INT PRIMARY KEY,
    bio TEXT,
    avatar BLOB
);

垂直分库

text

复制代码
原库:一个库包含订单、用户、商品所有表

拆分为:
- 订单库:订单表、订单详情表
- 用户库:用户表、地址表
- 商品库:商品表、库存表

水平拆分示例

sql

复制代码
-- 原表:订单表,数据量巨大(2 亿行)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME
);

-- 水平拆分:按 user_id 取模 8,分到 8 张表
orders_0, orders_1, orders_2, ..., orders_7

选择分片键的原则

  • 查询频率高:选择最常用的查询字段作为分片键

  • 数据分布均匀:避免数据倾斜

  • 稳定性:分片键的值不应该频繁变化


第四梯队:极客进阶题(出现率 15%+)


41. MySQL 8.0 有哪些重要新特性?

核心答案

MySQL 8.0 是里程碑版本,引入了大量重要特性。

详细说明

1. 窗口函数

sql

复制代码
-- 按部门排名
SELECT name, dept, salary,
       RANK() OVER (PARTITION BY dept ORDER BY salary DESC) as rank
FROM employees;

-- 累计求和
SELECT date, amount,
       SUM(amount) OVER (ORDER BY date) as running_total
FROM sales;

2. 通用表达式(CTE)

sql

复制代码
WITH RECURSIVE org_tree AS (
    SELECT id, name, parent_id, 1 as level
    FROM org WHERE parent_id IS NULL
    UNION ALL
    SELECT o.id, o.name, o.parent_id, ot.level + 1
    FROM org o
    JOIN org_tree ot ON o.parent_id = ot.id
)
SELECT * FROM org_tree;

3. 隐藏索引

sql

复制代码
-- 隐藏索引,不被优化器使用,可用于测试删除索引的影响
ALTER TABLE t ALTER INDEX idx_name INVISIBLE;

4. 降序索引

sql

复制代码
-- 真正支持降序索引(之前是反向扫描)
CREATE INDEX idx_name_age ON t (name ASC, age DESC);

5. 原子 DDL

  • DDL 操作变成原子操作,中断时会回滚,不会留下残留

6. 默认使用 ROW 格式的 binlog

  • 提升主从复制安全性

7. 资源组

  • 控制线程的 CPU 资源分配

8. 撤销表空间自动截断

  • 解决 undo log 膨胀问题

42. 如何监控 MySQL 性能?关键指标有哪些?

核心答案

MySQL 性能监控需要关注连接数、QPS/TPS、慢查询、锁等待、buffer pool 命中率、磁盘 I/O等核心指标。

详细说明

关键指标及查看方式

1. 连接数

sql

复制代码
SHOW STATUS LIKE 'Threads_connected';    -- 当前连接数
SHOW VARIABLES LIKE 'max_connections';   -- 最大连接数
-- 告警阈值:连接数 > 80% 最大连接数

2. QPS/TPS

sql

复制代码
-- QPS(每秒查询数)
SHOW STATUS LIKE 'Queries';
-- TPS(每秒事务数,基于 Com_commit 和 Com_rollback 计算)
SHOW STATUS LIKE 'Com_commit';
SHOW STATUS LIKE 'Com_rollback';

3. 慢查询

sql

复制代码
SHOW STATUS LIKE 'Slow_queries';         -- 慢查询数量
SHOW VARIABLES LIKE 'long_query_time';   -- 慢查询阈值

4. InnoDB 行锁等待

sql

复制代码
SHOW STATUS LIKE 'Innodb_row_lock_waits';    -- 锁等待次数
SHOW STATUS LIKE 'Innodb_row_lock_time';     -- 锁等待总时间
-- 告警阈值:锁等待次数持续增长

5. Buffer Pool 命中率

sql

复制代码
SHOW ENGINE INNODU STATUS\G
-- 查看 Buffer pool hit rate
-- 理想值:> 99%,低于 95% 说明内存不足

6. 主从延迟

sql

复制代码
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master
-- 告警阈值:> 60 秒

7. 磁盘 I/O

bash

复制代码
# 系统层面
iostat -x 1
# 关注 await、%util

监控工具

  • Prometheus + Grafana:开源监控方案

  • Percona Monitoring and Management:MySQL 专业监控

  • MySQL Enterprise Monitor:官方监控工具

  • 阿里云 RDS 监控:云产品自带


43. 什么是分区表?与分库分表有什么区别?

核心答案

分区表 是 MySQL 在单库单表层面的物理拆分,对应用透明,将一张表的数据按规则分散到多个物理文件中。

详细说明

分区类型

sql

复制代码
-- RANGE 分区
CREATE TABLE orders (
    id INT,
    create_date DATE
) PARTITION BY RANGE (YEAR(create_date)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025)
);

-- LIST 分区
PARTITION BY LIST (region) (
    PARTITION p_east VALUES IN ('北京','上海'),
    PARTITION p_west VALUES IN ('四川','重庆')
);

-- HASH 分区
PARTITION BY HASH (id) PARTITIONS 8;

-- KEY 分区
PARTITION BY KEY (user_id) PARTITIONS 8;

分区表 vs 分库分表

对比项 分区表 分库分表
物理存储 同一数据库,不同物理文件 不同数据库或不同实例
对应用透明 ✅ 完全透明 ❌ 需要应用感知或中间件
跨分区查询 ✅ 支持,优化器自动处理 ❌ 需要中间件聚合
事务支持 ✅ 完整支持 ❌ 分布式事务复杂
扩展性 有限(单机瓶颈) 好(可以无限扩展)
维护成本
适用场景 数据量大但单机能承载 数据量超大,需要水平扩展

分区表的限制

  • 分区数最大 8192

  • 所有分区必须在同一个 MySQL 实例

  • 外键约束不支持分区表

  • 分区键必须包含在主键或唯一索引中


44. 什么是 JSON 类型?如何使用?

核心答案

MySQL 5.7 开始支持 JSON 数据类型,可以存储和查询 JSON 格式的数据,支持 JSON 函数和索引(通过生成列)。

详细说明

创建 JSON 列

sql

复制代码
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    profile JSON
);

-- 插入 JSON 数据
INSERT INTO users VALUES (1, '张三', '{"age": 25, "city": "北京", "tags": ["程序猿", "运动"]}');

JSON 函数

sql

复制代码
-- 提取 JSON 字段
SELECT JSON_EXTRACT(profile, '$.age') FROM users;
SELECT profile->'$.age' FROM users;  -- 简化写法
SELECT profile->>'$.age' FROM users; -- 返回无引号字符串

-- 判断是否包含
SELECT * FROM users WHERE JSON_CONTAINS(profile->'$.tags', '"程序猿"');

-- 更新 JSON 字段
UPDATE users SET profile = JSON_SET(profile, '$.age', 26) WHERE id = 1;

-- 添加字段
UPDATE users SET profile = JSON_INSERT(profile, '$.email', 'zhangsan@example.com');

-- 删除字段
UPDATE users SET profile = JSON_REMOVE(profile, '$.age');

创建 JSON 索引(通过生成列):

sql

复制代码
-- 创建生成列
ALTER TABLE users ADD COLUMN age INT GENERATED ALWAYS AS (profile->>'$.age') STORED;
-- 在生成列上建索引
CREATE INDEX idx_age ON users(age);

适用场景

  • 存储非结构化数据

  • 字段经常变化,不适合固定表结构

  • 配置信息、用户扩展属性等


45. 什么是全文索引?如何使用?

核心答案

全文索引(Full-Text Index) 用于对文本内容进行关键词搜索,支持自然语言搜索和布尔搜索,比 LIKE 性能更好。

详细说明

创建全文索引

sql

复制代码

自然语言搜索

复制代码
-- 建表时创建
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,
    FULLTEXT(title, content)
) ENGINE=InnoDB;

-- 或单独添加
ALTER TABLE articles ADD FULLTEXT(title, content);

sql

复制代码
-- 按相关性排序
SELECT *, MATCH(title, content) AGAINST('MySQL 索引') AS score
FROM articles
WHERE MATCH(title, content) AGAINST('MySQL 索引')
ORDER BY score DESC;

布尔搜索

sql

复制代码
-- + 必须包含,- 必须不包含,* 通配符
SELECT * FROM articles
WHERE MATCH(content) AGAINST('+MySQL -Oracle 索引*' IN BOOLEAN MODE);

查询扩展

sql

复制代码
-- 自动扩展搜索词(基于相关文档)
SELECT * FROM articles
WHERE MATCH(content) AGAINST('MySQL' WITH QUERY EXPANSION);

注意事项

  • 最小搜索长度:InnoDB 默认 3 个字符(innodb_ft_min_token_size

  • 停用词:默认有一些常见词(the、and 等)被忽略

  • 中文需要设置 ngram 分词器:

sql

复制代码
SET GLOBAL innodb_ft_enable_stopword = OFF;
SET GLOBAL ngram_token_size = 2;

46. 什么是数据库中间件?常用的有哪些?

核心答案

数据库中间件 是位于应用程序和数据库之间的软件层,用于解决分库分表、读写分离、故障切换、数据聚合等问题。

详细说明

中间件分类

1. Proxy 模式(代理层)

  • 独立的代理服务,对应用透明

  • 优点:应用无侵入

  • 缺点:增加网络跳转,有一定性能损耗

中间件 特点
MyCat 国产,功能完善,社区活跃
ProxySQL 高性能,支持读写分离、查询缓存
MySQL Router 官方轻量级路由
Vitess YouTube 开源,支持大规模集群

2. JDBC 模式(应用层)

  • 集成在应用中,作为数据源代理

  • 优点:性能好,无需额外部署

  • 缺点:语言绑定,只支持 Java

中间件 特点
ShardingSphere-JDBC Apache 顶级项目,功能强大
TDDL 阿里开源(淘宝分布式数据层)
Zebra 美团开源

ShardingSphere 核心功能

  • 分库分表:支持取模、范围、哈希等多种分片策略

  • 读写分离:支持主从自动路由

  • 分布式事务:支持 XA 和 BASE

  • 数据加密:敏感数据自动加解密

  • 影子库:压测数据隔离


47. 如何保证数据库的高可用?

核心答案

MySQL 高可用方案主要有主从切换、MHA、MGR、InnoDB Cluster、云原生 RDS等。

详细说明

方案对比

方案 故障切换 数据一致性 复杂度 适用场景
主从 + 手动切换 手动,分钟级 可能丢数据 非核心业务
MHA 自动,10-30 秒 可能丢数据 传统主从架构
MGR 自动,秒级 强一致(组复制) MySQL 5.7+
InnoDB Cluster 自动,秒级 强一致 MySQL 官方方案
PXC / Galera 自动,秒级 强一致(同步复制) 对一致性要求极高
云 RDS 自动,秒级 高可用(云厂商保证) 上云场景

MHA(Master High Availability)

  • 成熟的主从切换方案

  • 监控主库,故障时选举新主库,自动补偿 binlog 差异

  • 需要配置 VIP 或 DNS 切换

MGR(MySQL Group Replication)

  • MySQL 5.7 引入,基于 Paxos 协议

  • 支持单主和多主模式

  • 自动故障切换,数据强一致

InnoDB Cluster

  • MySQL 官方高可用方案

  • 包含 MySQL Shell、MGR、MySQL Router

  • 配置简单,官方支持

云 RDS

  • 阿里云 RDS、腾讯云 MySQL、AWS RDS 等

  • 自动高可用(主备切换,数据备份)

  • 对应用无感知,降低运维成本


48. 如何备份和恢复 MySQL 数据?

核心答案

MySQL 备份分为逻辑备份物理备份 ,结合全量备份增量备份实现数据保护。

详细说明

备份类型

备份方式 工具 优点 缺点
逻辑备份 mysqldump, mydumper 跨平台,可编辑,备份 SQL 恢复慢,影响业务
物理备份 XtraBackup, MySQL Enterprise Backup 热备份,恢复快 依赖操作系统

常用备份工具

1. mysqldump(逻辑备份)

bash

复制代码
# 全量备份
mysqldump -u root -p --all-databases > backup.sql

# 备份单库
mysqldump -u root -p --single-transaction --master-data=2 mydb > mydb.sql

# 恢复
mysql -u root -p mydb < mydb.sql

2. Percona XtraBackup(物理热备份)

bash

复制代码
# 全量备份
xtrabackup --backup --target-dir=/backup/full

# 增量备份
xtrabackup --backup --target-dir=/backup/inc1 --incremental-basedir=/backup/full

# 准备恢复
xtrabackup --prepare --target-dir=/backup/full
xtrabackup --prepare --target-dir=/backup/full --incremental-dir=/backup/inc1

# 恢复
xtrabackup --copy-back --target-dir=/backup/full

备份策略

  • 全量备份:每周一次(业务低峰期)

  • 增量备份:每天一次

  • binlog 备份:实时备份,用于 PITR(Point-in-Time Recovery)

恢复验证

  • 定期在测试环境演练恢复流程

  • 验证备份文件的完整性

  • 记录恢复时间 RTO(恢复时间目标)


49. 如何选择合适的数据类型?

核心答案

数据类型选择影响存储空间、查询性能、索引效率,需要根据业务场景合理选择。

详细说明

整型选择

类型 字节数 范围 适用场景
TINYINT 1 -128~127 状态码、枚举
SMALLINT 2 -32768~32767 小范围数字
INT 4 -21亿~21亿 一般 ID、计数
BIGINT 8 9e18 大 ID、时间戳

原则:能用小整型不用大整型,节省空间。

字符串选择

类型 场景
CHAR 长度固定:手机号、身份证、MD5
VARCHAR 长度变化:用户名、地址
TEXT 长文本:文章内容、备注
BLOB 二进制:图片、文件(不建议存数据库)

原则:VARCHAR(255) 是常用值,255 以内长度前缀 1 字节。

时间类型选择

类型 字节数 范围 精度
DATE 3 1000-01-01 ~ 9999-12-31
DATETIME 8 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
TIMESTAMP 4 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07
TIME 3 -838:59:59 ~ 838:59:59

原则

  • TIMESTAMP 占 4 字节,但范围有限,2038 年问题

  • DATETIME 占 8 字节,范围更大

  • 存储毫秒级时间戳可用 BIGINT

DECIMAL vs FLOAT

  • DECIMAL:精确计算,适合金额、价格

  • FLOAT/DOUBLE:近似计算,适合科学计算


50. 什么是数据库连接数过高?如何排查?

核心答案

连接数过高指 MySQL 的并发连接数接近或超过 max_connections,可能导致连接失败、响应变慢、甚至服务不可用

详细说明

排查步骤

1. 查看当前连接数

sql

复制代码
SHOW STATUS LIKE 'Threads_connected';  -- 当前连接数
SHOW VARIABLES LIKE 'max_connections'; -- 最大连接数

2. 查看连接来源

sql

复制代码
-- 查看每个客户端 IP 的连接数
SELECT host, COUNT(*) FROM information_schema.processlist GROUP BY host;

-- 查看每个用户的状态
SELECT user, state, COUNT(*) FROM information_schema.processlist GROUP BY user, state;

3. 查看空闲连接

sql

复制代码
-- 查看长时间空闲的连接
SELECT id, user, host, command, time, state, info
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60;

4. 查看慢查询或锁等待

sql

复制代码
-- 查看正在执行的慢查询
SELECT * FROM information_schema.processlist WHERE time > 10 AND command != 'Sleep';

常见原因及解决方案

原因 解决方案
连接池配置过大 调整 maximumPoolSize,不超过数据库最大连接数
连接泄漏(未归还) 检查代码,确保 finally 中关闭连接
慢查询堆积 优化 SQL,增加索引
长事务未提交 检查事务,确保及时提交
突发流量 扩容、限流、增加连接数上限

紧急处理

sql

复制代码
-- 杀掉空闲超时的连接
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300;

-- 临时增大连接数
SET GLOBAL max_connections = 500;

以上是 50 道 MySQL 高频面试题的详细解答,覆盖了从基础到进阶的各个知识点。建议结合自己的项目经验,理解每个知识点背后的原理,而不是死记硬背。面试时如果能结合实际场景举例,会更加分。祝你面试顺利!

相关推荐
lierenvip7 小时前
mysql的主从配置
android·mysql·adb
野生技术架构师7 小时前
制造业数据库选型实战:为什么我们从 MySQL 迁移到 TiDB
数据库·mysql·tidb
紫丁香8 小时前
高并发面试3
后端·面试·高并发·场景
黄昏回响8 小时前
计算机系统基础知识(十):软件篇之中间件详解
中间件·面试·职场和发展·改行学it
AI成长日志8 小时前
【笔面试算法学习专栏】图算法入门专题:岛屿数量与课程表
学习·算法·面试
Java面试题总结8 小时前
MySQL高级SQL秘籍:性能飞升之路
sql·mysql·adb
郝学胜-神的一滴8 小时前
从线程栈到表达式求值:栈结构的核心应用与递归实现
开发语言·数据结构·c++·算法·面试·职场和发展·软件工程
敲代码的嘎仔8 小时前
Java后端开发——多线程面试题
java·开发语言·面试·多线程·八股·threadlocal·
qq5680180768 小时前
mysql数据被误删的恢复方案
数据库·mysql
鬼才血脉8 小时前
CentOS 7 安装 MySQL 8.0
linux·mysql·centos